diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8d57d8fa..69f30498 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - v1 tags: - '*' pull_request: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a3462b48..44f9e137 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,9 +4,11 @@ on: pull_request: branches: - main + - v1 push: branches: - main + - v1 tags: - '*' diff --git a/docs/src/conversion-to-julia.md b/docs/src/conversion-to-julia.md index 49539d4a..e6536f56 100644 --- a/docs/src/conversion-to-julia.md +++ b/docs/src/conversion-to-julia.md @@ -11,7 +11,7 @@ From Python, the arguments to a Julia function will be converted according to th | From | To | | :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- | | **Top priority (wrapped values).** | | -| `juliacall.AnyValue` | `Any` | +| `juliacall.Jl` | `Any` | | **Very high priority (arrays).** | | | Objects satisfying the buffer or array interface (inc. `bytes`, `bytearray`, `array.array`, `numpy.ndarray`) | `PyArray` | | **High priority (canonical conversions).** | | diff --git a/docs/src/conversion-to-python.md b/docs/src/conversion-to-python.md index bb38ec5a..37c25d92 100644 --- a/docs/src/conversion-to-python.md +++ b/docs/src/conversion-to-python.md @@ -23,16 +23,13 @@ From Python, this occurs when converting the return value of a Julia function. | Standard integer range (`AbstractRange{T}`, `T` a standard integer) | `range` | | `Date`, `Time`, `DateTime` (from `Dates`) | `date`, `time`, `datetime` (from `datetime`) | | `Second`, `Millisecond`, `Microsecond`, `Nanosecond` (from `Dates`) | `timedelta` (from `datetime`) | -| `Number` | `juliacall.NumberValue`, `juliacall.ComplexValue`, etc. | -| `AbstractArray` | `juliacall.ArrayValue`, `juliacall.VectorValue` | -| `AbstractDict` | `juliacall.DictValue` | -| `AbstractSet` | `juliacall.SetValue` | -| `IO` | `juliacall.BufferedIOValue` | -| `Module` | `juliacall.ModuleValue` | -| `Type` | `juliacall.TypeValue` | -| Anything else | `juliacall.AnyValue` | +| `AbstractArray` | `juliacall.JlArray`, `juliacall.JlVector` | +| `AbstractDict` | `juliacall.JlDict` | +| `AbstractSet` | `juliacall.JlSet` | +| `IO` | `juliacall.JlBinaryIO` | +| Anything else | `juliacall.Jl` | -See [here](@ref julia-wrappers) for an explanation of the `juliacall.*Value` wrapper types. +See [here](@ref julia-wrappers) for an explanation of the `juliacall.Jl*` wrapper types. ## [Custom rules](@id jl2py-conversion-custom) @@ -44,11 +41,3 @@ object, then also define `ispy(::T) = true`. ```@docs PythonCall.ispy ``` - -Alternatively, if you define a wrapper type (a subtype of -[`juliacall.AnyValue`](#juliacall.AnyValue)) then you may instead define `pyjltype(::T)` to -be that type. - -```@docs -PythonCall.pyjltype -``` diff --git a/docs/src/faq.md b/docs/src/faq.md index 1cbcb207..1d3532c8 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -20,7 +20,7 @@ Related issues: [#201](https://github.com/JuliaPy/PythonCall.jl/issues/201), [#2 ## Issues when Numpy arrays are expected -When a Julia array is passed to Python, it is wrapped as a [`ArrayValue`](#juliacall.ArrayValue). +When a Julia array is passed to Python, it is wrapped as a [`JlArray`](#juliacall.JlArray). This type satisfies the Numpy array interface and the buffer protocol, so can be used in most places where a numpy array is valid. diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md index c62a480b..d646b421 100644 --- a/docs/src/juliacall-reference.md +++ b/docs/src/juliacall-reference.md @@ -5,7 +5,7 @@ `````@customdoc juliacall.Main - Constant -The Julia `Main` module, as a [`ModuleValue`](#juliacall.ModuleValue). +The Julia `Main` module, as a [`Jl`](#juliacall.Jl). In interactive scripts, you can use this as the main entry-point to JuliaCall: ```python @@ -20,18 +20,6 @@ The modules `Base`, `Core` and `PythonCall` are also available. ## Utilities -`````@customdoc -juliacall.convert - Function - -```python -convert(T, x) -``` - -Convert `x` to a Julia object of type `T`. - -You can use this to pass an argument to a Julia function of a specific type. -````` - `````@customdoc juliacall.newmodule - Function @@ -45,74 +33,70 @@ A new module with the given name. ## [Wrapper types](@id julia-wrappers) Apart from a few fundamental immutable types, all Julia values are by default converted into -Python to some [`AnyValue`](#juliacall.AnyValue) object, which wraps the original value, but -giving it a Pythonic interface. - -Subclasses of [`AnyValue`](#juliacall.AnyValue) provide additional Python semantics. For -example a Julia vector is converted to a [`VectorValue`](#juliacall.VectorValue) which -satisfies the Python sequence interface and behaves very similar to a list. - -There is also a [`RawValue`](#juliacall.RawValue) object, which gives a stricter -"Julia-only" interface, documented below. These types all inherit from `ValueBase`: - -- `ValueBase` - - [`RawValue`](#juliacall.RawValue) - - [`AnyValue`](#juliacall.AnyValue) - - [`NumberValue`](#juliacall.NumberValue) - - `ComplexValue` - - `RealValue` - - `RationalValue` - - `IntegerValue` - - [`ArrayValue`](#juliacall.ArrayValue) - - [`VectorValue`](#juliacall.VectorValue) - - [`DictValue`](#juliacall.DictValue) - - [`SetValue`](#juliacall.SetValue) - - [`IOValue`](#juliacall.IOValue) - - `BinaryIOValue` - - `TextIOValue` - - [`ModuleValue`](#juliacall.ModuleValue) - - [`TypeValue`](#juliacall.TypeValue) +Python to a [`Jl`](#juliacall.Jl) object, which wraps the original value and gives it a +Pythonic interface. + +Other wrapper classes provide more specific Python semantics. For example a Julia vector can +be converted to a [`JlVector`](#juliacall.JlVector) which satisfies the Python sequence +interface and behaves very similar to a list. + +- `JlBase` + - [`Jl`](#juliacall.Jl) + - [`JlCollection`](#juliacall.JlCollection) + - [`JlArray`](#juliacall.JlArray) + - [`JlVector`](#juliacall.JlVector) + - [`JlDict`](#juliacall.JlDict) + - [`JlSet`](#juliacall.JlSet) + - [`JlIOBase`](#juliacall.JlIOBase) + - `JlBinaryIO` + - `JlTextIO` `````@customdoc -juliacall.AnyValue - Class +juliacall.Jl - Class -Wraps any Julia object, giving it some basic Python semantics. Subtypes provide extra -semantics. +Wraps any Julia object, giving it some basic Python semantics. Supports `repr(x)`, `str(x)`, attributes (`x.attr`), calling (`x(a,b)`), iteration, comparisons, `len(x)`, `a in x`, `dir(x)`. -Calling, indexing, attribute access, etc. will convert the result to a Python object -according to [this table](@ref jl2py). This is typically a builtin Python type (for -immutables) or a subtype of `AnyValue`. +Calling, indexing, attribute access, etc. will always return a `Jl`. To get the result +as an ordinary Python object, you can use the `.jl_to_py()` method. -Attribute access can be used to access Julia properties as well as normal class members. In -the case of a name clash, the class member will take precedence. For convenience with Julia -naming conventions, `_b` at the end of an attribute is replaced with `!` and `_bb` is -replaced with `!!`. +Attribute access (`x.attr`) can be used to access Julia properties except those starting +and ending with `__` (since these are Python special methods) or starting with `jl_` or +`_jl_` (which are reserved by `juliacall` for Julia-specific methods). ###### Members -- `_jl_raw()`: Convert to a [`RawValue`](#juliacall.RawValue). (See also [`pyjlraw`](@ref).) -- `_jl_display()`: Display the object using Julia's display mechanism. -- `_jl_help()`: Display help for the object. +- `jl_callback(*args, **kwargs)`: Calls the Julia object with the given arguments. + Unlike ordinary calling syntax, the arguments are passed as `Py` objects instead of + being converted. +- `jl_display()`: Display the object using Julia's display mechanism. +- `jl_eval(expr)`: If the object is a Julia `Module`, evaluates the given expression. +- `jl_help()`: Display help for the object. +- `jl_to_py()`: Convert to a Python object using the [usual conversion rules](@ref jl2py). ````` `````@customdoc -juliacall.NumberValue - Class +juliacall.JlCollection - Class + +Wraps any Julia collection. It is a subclass of `collections.abc.Collection`. -This wraps any Julia `Number` value. It is a subclass of `numbers.Number` and behaves -similar to other Python numbers. +Julia collections are arrays, sets, dicts, tuples, named tuples, refs, and in general +anything which is a collection of values in the sense that it supports functions like +`iterate`, `in`, `length`, `hash`, `==`, `isempty`, `copy`, `empty!`. -There are also subtypes `ComplexValue`, `RealValue`, `RationalValue`, `IntegerValue` which -wrap values of the corresponding Julia types, and are subclasses of the corresponding -`numbers` ABC. +It supports `in`, `iter`, `len`, `hash`, `bool`, `==`. + +###### Members +- `clear()`: Empty the collection in-place. +- `copy()`: A copy of the collection. ````` `````@customdoc -juliacall.ArrayValue - Class +juliacall.JlArray - Class This wraps any Julia `AbstractArray` value. It is a subclass of -`collections.abc.Collection`. +`juliacall.JlCollection`. It supports zero-up indexing, and can be indexed with integers or slices. Slicing returns a view of the original array. @@ -130,22 +114,20 @@ copy of the original array. ###### Members - `ndim`: The number of dimensions. - `shape`: Tuple of lengths in each dimension. -- `copy()`: A copy of the array. - `reshape(shape)`: A reshaped view of the array. - `to_numpy(dtype=None, copy=True, order="K")`: Convert to a numpy array. ````` `````@customdoc -juliacall.VectorValue - Class +juliacall.JlVector - Class -This wraps any Julia `AbstractVector` value. It is a subclass of `juliacall.ArrayValue` and +This wraps any Julia `AbstractVector` value. It is a subclass of `juliacall.JlArray` and `collections.abc.MutableSequence` and behaves similar to a Python `list`. ###### Members - `resize(size)`: Change the length of the vector. - `sort(reverse=False, key=None)`: Sort the vector in-place. - `reverse()`: Reverse the vector. -- `clear()`: Empty the vector. - `insert(index, value)`: Insert the value at the given index. - `append(value)`: Append the value to the end of the vector. - `extend(values)`: Append the values to the end of the vector. @@ -156,65 +138,23 @@ This wraps any Julia `AbstractVector` value. It is a subclass of `juliacall.Arra ````` `````@customdoc -juliacall.DictValue - Class +juliacall.JlDict - Class This wraps any Julia `AbstractDict` value. It is a subclass of `collections.abc.Mapping` and behaves similar to a Python `dict`. ````` `````@customdoc -juliacall.SetValue - Class +juliacall.JlSet - Class This wraps any Julia `AbstractSet` value. It is a subclass of `collections.abc.Set` and behaves similar to a Python `set`. ````` `````@customdoc -juliacall.IOValue - Class +juliacall.JlIOBase - Class This wraps any Julia `IO` value. It is a subclass of `io.IOBase` and behaves like Python files. -There are also subtypes `BinaryIOValue` and `TextIOValue`, which are subclasses of +There are also subtypes `JlBinaryIO` and `JlTextIO`, which are subclasses of `io.BufferedIOBase` (buffered bytes) and `io.TextIOBase` (text). ````` - -`````@customdoc -juliacall.ModuleValue - Class -This wraps any Julia `Module` value. - -It is the same as [`AnyValue`](#juliacall.AnyValue) except for one additional convenience -method: -- `seval([module=self], code)`: Evaluates the given code (a string) in the given module. -````` - -`````@customdoc -juliacall.TypeValue - Class - -This wraps any Julia `Type` value. - -It is the same as [`AnyValue`](#juliacall.AnyValue) except that indexing is used to access -Julia's "curly" syntax for specifying parametric types: - -```python -from juliacall import Main as jl -# equivalent to Vector{Int}() in Julia -jl.Vector[jl.Int]() -``` -````` - -`````@customdoc -juliacall.RawValue - Class - -Wraps any Julia value with a rigid interface suitable for generic programming. - -Supports `repr(x)`, `str(x)`, attributes (`x.attr`), calling (`x(a,b)`), `len(x)`, `dir(x)`. - -This is very similar to [`AnyValue`](#juliacall.AnyValue) except that indexing, calling, -etc. will always return a `RawValue`. - -Indexing with a tuple corresponds to indexing in Julia with multiple values. To index with a -single tuple, it will need to be wrapped in another tuple. - -###### Members -- `_jl_any()`: Convert to a [`AnyValue`](#juliacall.AnyValue) (or subclass). (See also - [`pyjl`](@ref).) -````` diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md index a16b71f2..678a747d 100644 --- a/docs/src/juliacall.md +++ b/docs/src/juliacall.md @@ -50,11 +50,11 @@ import juliacall jl = juliacall.newmodule("SomeName") ``` -Julia modules have a special method `seval` which will evaluate a given piece of code given +Julia modules have a special method `jl_eval` which will evaluate a given piece of code given as a string in the module. This is most frequently used to import modules: ```python from array import array -jl.seval("using Statistics") +jl.jl_eval("using Statistics") x = array('i', [1, 2, 3]) jl.mean(x) # 2.0 @@ -64,7 +64,7 @@ jl.cor(x, y) ``` What to read next: -- The main functionality of this package is in `AnyValue` objects, which represent Julia +- The main functionality of this package is in `Jl` objects, which represent Julia objects, [documented here](@ref julia-wrappers). - If you need to install Julia packages, [read here](@ref julia-deps). - When you call a Julia function, such as `jl.rand(...)` in the above example, its diff --git a/docs/src/pycall.md b/docs/src/pycall.md index e96f89dd..740b89ff 100644 --- a/docs/src/pycall.md +++ b/docs/src/pycall.md @@ -26,7 +26,7 @@ Furthermore, `pyconvert` can be extended to support more types, whereas `convert Both packages allow conversion of Julia values to Python: `PyObject(x)` in PyCall, `Py(x)` in PythonCall. -Whereas both packages convert numbers, booleans, tuples and strings to their Python counterparts, they differ in handling other types. For example PyCall converts `AbstractVector` to `list` whereas PythonCall converts `AbstractVector` to `juliacall.VectorValue` which is a sequence type directly wrapping the Julia value - this has the advantage that mutating the Python object also mutates the original Julia object. +Whereas both packages convert numbers, booleans, tuples and strings to their Python counterparts, they differ in handling other types. For example PyCall converts `AbstractVector` to `list` whereas PythonCall converts `AbstractVector` to `juliacall.JlVector` which is a sequence type directly wrapping the Julia value - this has the advantage that mutating the Python object also mutates the original Julia object. Hence with PyCall the following does not mutate the original array `x`: ```julia diff --git a/docs/src/pythoncall-reference.md b/docs/src/pythoncall-reference.md index 75065f50..e0a42d4b 100644 --- a/docs/src/pythoncall-reference.md +++ b/docs/src/pythoncall-reference.md @@ -88,11 +88,13 @@ conversion to Python, unless the value is immutable and has a corresponding Pyth ```@docs pyjl -pyjlraw pyisjl pyjlvalue pybinaryio pytextio +pyjlarray +pyjlset +pyjldict ``` ## Arithmetic diff --git a/docs/src/releasenotes.md b/docs/src/releasenotes.md index 3b0cb248..25d3374d 100644 --- a/docs/src/releasenotes.md +++ b/docs/src/releasenotes.md @@ -2,6 +2,17 @@ ## Unreleased (v1) * `PythonCall.GC` is now more like `Base.GC`: `enable(true)` replaces `enable()`, `enable(false)` replaces `disable()`, and `gc()` is added. +* Breaking changes to Julia wrapper types: + * Classes renamed: `ValueBase` to `JlBase`, `AnyValue` to `Jl`, `ArrayValue` to `JlArray`, etc. + * Classes removed: `RawValue`, `ModuleValue`, `TypeValue`, `NumberValue`, `ComplexValue`, `RealValue`, `RationalValue`, `IntegerValue`. + * `Jl` now behaves similar to how `RawValue` behaved before. In particular, most methods on `Jl` now return a `Jl` instead of an arbitrary Python object. + * `juliacall.Pkg` removed (you can import it yourself). + * `juliacall.convert` removed (use `juliacall.Jl` instead). + * Methods renamed: `_jl_display()` to `jl_display()`, `_jl_help()` to `jl_help()`, etc. + * Methods removed: `_jl_raw()`. + * `pyjl(x)` now always returns a `juliacall.Jl` (it used to select a wrapper type if possible). + * `pyjltype(x)` removed. +* New functions: `pyjlarray`, `pyjldict`, `pyjlset`. ## Unreleased * `Serialization.serialize` can use `dill` instead of `pickle` by setting the env var `JULIA_PYTHONCALL_PICKLE=dill`. diff --git a/examples/flux.ipynb b/examples/flux.ipynb index c15d8d17..181b3090 100644 --- a/examples/flux.ipynb +++ b/examples/flux.ipynb @@ -31,14 +31,14 @@ "metadata": {}, "outputs": [], "source": [ - "jl.seval(\"using Flux\")\n", + "jl.jl_eval(\"using Flux\")\n", "model = jl.Chain(\n", " jl.Dense(1, 10, jl.relu),\n", " jl.Dense(10, 10, jl.relu),\n", " jl.Dense(10, 10, jl.relu),\n", " jl.Dense(10, 1),\n", ")\n", - "loss = jl.seval(\"m -> (x, y) -> Flux.Losses.mse(m(x), y)\")(model)" + "loss = jl.jl_eval(\"m -> (x, y) -> Flux.Losses.mse(m(x), y)\")(model)" ] }, { diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py index 66bc34e5..9b7133d1 100644 --- a/pysrc/juliacall/__init__.py +++ b/pysrc/juliacall/__init__.py @@ -9,18 +9,9 @@ def newmodule(name): "A new module with the given name." global _newmodule if _newmodule is None: - _newmodule = Main.seval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)") + _newmodule = Main.jl_eval("name -> (n1=Symbol(name); n2=gensym(n1); Main.@eval(module $n2; module $n1; end; end); Main.@eval $n2.$n1)") return _newmodule(name) -_convert = None - -def convert(T, x): - "Convert x to a Julia T." - global _convert - if _convert is None: - _convert = PythonCall.JlWrap.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))") - return _convert(T, x) - def interactive(enable=True): "Allow the Julia event loop to run in the background of the Python REPL." if enable: diff --git a/pysrc/juliacall/importer.py b/pysrc/juliacall/importer.py index 6aea2750..c1bd7a92 100644 --- a/pysrc/juliacall/importer.py +++ b/pysrc/juliacall/importer.py @@ -83,7 +83,7 @@ def gen_file(jl, py): def exec_module(name, code): pymod = sys.modules[name] jlmod = newmodule(name) - jlmod.seval('begin\n' + code + '\nend') + jlmod.jl_eval('begin\n' + code + '\nend') delattr(pymod, 'juliacall') setattr(pymod, '__jl_code__', code) setattr(pymod, '__jl_module__', jlmod) diff --git a/pysrc/juliacall/ipython.py b/pysrc/juliacall/ipython.py index 7edc43a4..cc774d68 100644 --- a/pysrc/juliacall/ipython.py +++ b/pysrc/juliacall/ipython.py @@ -16,9 +16,9 @@ from . import Main, PythonCall import __main__ -_set_var = Main.seval("(k, v) -> @eval $(Symbol(k)) = $v") -_get_var = Main.seval("k -> hasproperty(Main, Symbol(k)) ? PythonCall.pyjlraw(getproperty(Main, Symbol(k))) : nothing") -_egal = Main.seval("===") +_set_var = Main.jl_eval("(k, v) -> @eval $(Symbol(k)) = $v") +_get_var = Main.jl_eval("k -> hasproperty(Main, Symbol(k)) ? PythonCall.pyjlraw(getproperty(Main, Symbol(k))) : nothing") +_egal = Main.jl_eval("===") @magics_class class JuliaMagics(Magics): @@ -49,7 +49,7 @@ def julia(self, line, cell=None): if k in syncvars: cachevars[k] = _get_var(k) # run the code - ans = Main.seval('begin\n' + code + '\nend') + ans = Main.jl_eval('begin\n' + code + '\nend') # flush stderr/stdout PythonCall._ipython._flush_stdio() # copy variables back to Python @@ -61,7 +61,7 @@ def julia(self, line, cell=None): __main__.__dict__[k] = v1._jl_any() # return the value unless suppressed with trailing ";" if not code.strip().endswith(';'): - return ans + return ans.jl_to_py() def load_ipython_extension(ip): # register magics @@ -69,7 +69,7 @@ def load_ipython_extension(ip): # redirect stdout/stderr if ip.__class__.__name__ == 'TerminalInteractiveShell': # no redirection in the terminal - PythonCall.seval("""module _ipython + PythonCall.jl_eval("""module _ipython function _flush_stdio() end end""") @@ -77,7 +77,7 @@ def load_ipython_extension(ip): # In Julia 1.7+ redirect_stdout() returns a Pipe object. Earlier versions of Julia # just return a tuple of the two pipe ends. This is why we have [1] and [2] below. # They can be dropped on earlier versions. - PythonCall.seval("""module _ipython + PythonCall.jl_eval("""module _ipython using ..PythonCall const _redirected_stdout = redirect_stdout() const _redirected_stderr = redirect_stderr() @@ -98,7 +98,7 @@ def load_ipython_extension(ip): end""") ip.events.register('post_execute', PythonCall._ipython._flush_stdio) # push displays - PythonCall.seval("""begin + PythonCall.jl_eval("""begin pushdisplay(Compat.PythonDisplay()) pushdisplay(Compat.IPythonDisplay()) nothing diff --git a/pytest/test_all.py b/pytest/test_all.py index c6cff009..00303587 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -5,18 +5,10 @@ def test_newmodule(): import juliacall jl = juliacall.Main m = juliacall.newmodule("TestModule") - assert isinstance(m, juliacall.ModuleValue) + assert isinstance(m, juliacall.Jl) assert jl.isa(m, jl.Module) assert str(jl.nameof(m)) == "TestModule" -def test_convert(): - import juliacall - jl = juliacall.Main - for (x, t) in [(None, jl.Nothing), (True, jl.Bool), ([1,2,3], jl.Vector)]: - y = juliacall.convert(t, x) - assert isinstance(y, juliacall.AnyValue) - assert jl.isa(y, t) - def test_interactive(): import juliacall juliacall.interactive(True) @@ -47,26 +39,26 @@ def test_issue_394(): f = lambda x: x+1 y = 5 jl.x = x - assert jl.x is x + assert jl.x.jl_to_py() is x jl.f = f - assert jl.f is f + assert jl.f.jl_to_py() is f jl.y = y - assert jl.y is y - assert jl.x is x - assert jl.f is f - assert jl.y is y - assert jl.seval("f(x)") == 4 + assert jl.y.jl_to_py() is y + assert jl.x.jl_to_py() is x + assert jl.f.jl_to_py() is f + assert jl.y.jl_to_py() is y + assert jl.jl_eval("f(x)").jl_to_py() == 4 def test_issue_433(): "https://github.com/JuliaPy/PythonCall.jl/issues/433" from juliacall import Main as jl # Smoke test - jl.seval("x=1\nx=1") + jl.jl_eval("x=1\nx=1") assert jl.x == 1 # Do multiple things - out = jl.seval( + out = jl.jl_eval( """ function _issue_433_g(x) return x^2 diff --git a/src/C/C.jl b/src/C/C.jl index 4ceda829..01bc7a4c 100644 --- a/src/C/C.jl +++ b/src/C/C.jl @@ -8,7 +8,6 @@ module C using Base: @kwdef using UnsafePointers: UnsafePtr using CondaPkg: CondaPkg -using Pkg: Pkg using Requires: @require using Libdl: dlpath, dlopen, dlopen_e, dlclose, dlsym, dlsym_e, RTLD_LAZY, RTLD_DEEPBIND, RTLD_GLOBAL diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl index e90dc478..e4215ac3 100644 --- a/src/JlWrap/C.jl +++ b/src/JlWrap/C.jl @@ -2,6 +2,8 @@ module Cjl using ...C: C using ...Utils: Utils +using ...Core: incref, pynew +using ...Convert: pyconvert using Base: @kwdef using UnsafePointers: UnsafePtr using Serialization: serialize, deserialize @@ -13,6 +15,7 @@ using Serialization: serialize, deserialize end const PyJuliaBase_Type = Ref(C.PyNULL) +const PyJuliaBase_New = Ref(C.PyNULL) # we store the actual julia values here # the `value` field of `PyJuliaValueObject` indexes into here @@ -30,7 +33,7 @@ end function _pyjl_dealloc(o::C.PyPtr) idx = UnsafePtr{PyJuliaValueObject}(o).value[] - if idx != 0 + if idx >= 1 PYJLVALUES[idx] = nothing push!(PYJLFREEVALUES, idx) end @@ -39,6 +42,60 @@ function _pyjl_dealloc(o::C.PyPtr) nothing end +function _getany(ptr::C.PyPtr) + if PyJuliaValue_Check(ptr) == 1 + PyJuliaValue_GetValue(ptr) + else + pyconvert(Any, pynew(incref(ptr))) + end +end + +function _getany(::Type{T}, ptr::C.PyPtr) where {T} + if PyJuliaValue_Check(ptr) == 1 + convert(T, PyJuliaValue_GetValue(ptr))::T + else + pyconvert(T, pynew(incref(ptr)))::T + end +end + +function _pyjl_init(xptr::C.PyPtr, argsptr::C.PyPtr, kwargsptr::C.PyPtr) + if kwargsptr != C.PyNULL && C.PyDict_Size(kwargsptr) != 0 + errset(pybuiltins.TypeError, "keyword arguments not allowed") + return Cint(-1) + end + if argsptr == C.PyNULL + return Cint(0) + end + nargs = C.PyTuple_Size(argsptr) + if nargs == 0 + return Cint(0) + elseif nargs > 2 + errset(pybuiltins.TypeError, "__init__() takes up to 2 arguments ($nargs given)") + return Cint(-1) + end + vptr = C.PyTuple_GetItem(argsptr, 0) + try + if nargs == 1 + v = _getany(vptr) + else + tptr = C.PyTuple_GetItem(argsptr, 1) + t = _getany(tptr) + if !isa(t, Type) + C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "type argument must be a Julia 'Type', not '$(typeof(t))'") + return Cint(-1) + end + v = _getany(t, vptr) + end + PyJuliaValue_SetValue(xptr, v) + Cint(0) + catch exc + errtype = exc isa MethodError ? C.POINTERS.PyExc_TypeError : C.POINTERS.PyExc_Exception + errmsg = sprint(showerror, exc) + C.PyErr_SetString(errtype, errmsg) + Cint(-1) + end +end + const PYJLMETHODS = Vector{Any}() function PyJulia_MethodNum(f) @@ -47,12 +104,6 @@ function PyJulia_MethodNum(f) return length(PYJLMETHODS) end -function _pyjl_isnull(o::C.PyPtr, ::C.PyPtr) - ans = PyJuliaValue_IsNull(o) ? C.POINTERS._Py_TrueStruct : C.POINTERS._Py_FalseStruct - C.Py_IncRef(ans) - ans -end - function _pyjl_callmethod(o::C.PyPtr, args::C.PyPtr) nargs = C.PyTuple_Size(args) @assert nargs > 0 @@ -251,9 +302,8 @@ function _pyjl_deserialize(t::C.PyPtr, v::C.PyPtr) end end -const _pyjlbase_name = "juliacall.ValueBase" +const _pyjlbase_name = "juliacall.JlBase" const _pyjlbase_type = fill(C.PyTypeObject()) -const _pyjlbase_isnull_name = "_jl_isnull" const _pyjlbase_callmethod_name = "_jl_callmethod" const _pyjlbase_reduce_name = "__reduce__" const _pyjlbase_serialize_name = "_jl_serialize" @@ -269,11 +319,6 @@ function init_c() meth = @cfunction(_pyjl_callmethod, C.PyPtr, (C.PyPtr, C.PyPtr)), flags = C.Py_METH_VARARGS, ), - C.PyMethodDef( - name = pointer(_pyjlbase_isnull_name), - meth = @cfunction(_pyjl_isnull, C.PyPtr, (C.PyPtr, C.PyPtr)), - flags = C.Py_METH_NOARGS, - ), C.PyMethodDef( name = pointer(_pyjlbase_reduce_name), meth = @cfunction(_pyjl_reduce, C.PyPtr, (C.PyPtr, C.PyPtr)), @@ -298,21 +343,25 @@ function init_c() _pyjlbase_type[] = C.PyTypeObject( name = pointer(_pyjlbase_name), basicsize = sizeof(PyJuliaValueObject), - # new = C.POINTERS.PyType_GenericNew, new = @cfunction(_pyjl_new, C.PyPtr, (C.PyPtr, C.PyPtr, C.PyPtr)), dealloc = @cfunction(_pyjl_dealloc, Cvoid, (C.PyPtr,)), + init = @cfunction(_pyjl_init, Cint, (C.PyPtr, C.PyPtr, C.PyPtr)), flags = C.Py_TPFLAGS_BASETYPE | C.Py_TPFLAGS_HAVE_VERSION_TAG, weaklistoffset = fieldoffset(PyJuliaValueObject, 3), - # getattro = C.POINTERS.PyObject_GenericGetAttr, - # setattro = C.POINTERS.PyObject_GenericSetAttr, methods = pointer(_pyjlbase_methods), as_buffer = pointer(_pyjlbase_as_buffer), ) o = PyJuliaBase_Type[] = C.PyPtr(pointer(_pyjlbase_type)) if C.PyType_Ready(o) == -1 C.PyErr_Print() - error("Error initializing 'juliacall.ValueBase'") + error("Error initializing 'juliacall.JlBase'") + end + n = PyJuliaBase_New[] = C.PyObject_GetAttrString(o, "__new__") + if n == C.PyNULL + C.PyErr_Print() + error("Error accessing 'juliacall.JlBase.__new__'") end + nothing end function __init__() @@ -321,13 +370,46 @@ function __init__() end end -PyJuliaValue_IsNull(o::C.PyPtr) = UnsafePtr{PyJuliaValueObject}(o).value[] == 0 +PyJuliaValue_Check(o::C.PyPtr) = C.PyObject_IsInstance(o, PyJuliaBase_Type[]) + +function PyJuliaValue_GetValue(o::C.PyPtr) + v = UnsafePtr{PyJuliaValueObject}(o).value[] + if v == 0 + nothing + elseif v > 0 + PYJLVALUES[v] + elseif v == -1 + false + elseif v == -2 + true + end +end + +function PyJuliaValue_SetValue(o::C.PyPtr, v::Union{Nothing,Bool}) + idx = UnsafePtr{PyJuliaValueObject}(o).value[] + if idx >= 1 + PYJLVALUES[idx] = nothing + push!(PYJLFREEVALUES, idx) + end + if v === nothing + idx = 0 + elseif v === false + idx = -1 + elseif v === true + idx = -2 + else + @assert false + end + UnsafePtr{PyJuliaValueObject}(o).value[] = idx + nothing +end -PyJuliaValue_GetValue(o::C.PyPtr) = PYJLVALUES[UnsafePtr{PyJuliaValueObject}(o).value[]] -PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v)) = begin +function PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v)) idx = UnsafePtr{PyJuliaValueObject}(o).value[] - if idx == 0 + if idx >= 1 + PYJLVALUES[idx] = v + else if isempty(PYJLFREEVALUES) push!(PYJLVALUES, v) idx = length(PYJLVALUES) @@ -336,18 +418,27 @@ PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v)) = begin PYJLVALUES[idx] = v end UnsafePtr{PyJuliaValueObject}(o).value[] = idx - else - PYJLVALUES[idx] = v end nothing end -PyJuliaValue_New(t::C.PyPtr, @nospecialize(v)) = begin +function PyJuliaValue_New(t::C.PyPtr, @nospecialize(v)) if C.PyType_IsSubtype(t, PyJuliaBase_Type[]) != 1 - C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.ValueBase'") + C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.JlBase'") return C.PyNULL end - o = C.PyObject_CallObject(t, C.PyNULL) + # All of this just to do JuliaBase.__new__(t). We do this to avoid calling `__init__` + # which itself sets the value, and so duplicates work. Some classes such as `JlArray` do + # not allow calling `__init__` with no args. + # TODO: it could be replaced with PyObject_CallOneArg(PyJuliaBase_New[], t) when we drop + # support for Python 3.8. + args = C.PyTuple_New(1) + args == C.PyNULL && return C.PyNULL + C.Py_IncRef(t) + err = C.PyTuple_SetItem(args, 0, t) + err == -1 && (C.Py_DecRef(args); return C.PyNULL) + o = C.PyObject_CallObject(PyJuliaBase_New[], args) + C.Py_DecRef(args) o == C.PyNULL && return C.PyNULL PyJuliaValue_SetValue(o, v) return o diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl index 498cc382..3f2e98b9 100644 --- a/src/JlWrap/JlWrap.jl +++ b/src/JlWrap/JlWrap.jl @@ -1,7 +1,7 @@ """ module PythonCall.JlWrap -Defines the Python object wrappers around Julia objects (`juliacall.AnyValue` etc). +Defines the Python object wrappers around Julia objects (`juliacall.Jl` etc). """ module JlWrap @@ -11,21 +11,16 @@ using ..Core: C, Utils, pynew, @autopy, incref, decref, setptr!, getptr, pyjulia using ..Convert: pyconvert, @pyconvert, PYCONVERT_PRIORITY_WRAP, pyconvert_add_rule, pyconvert_tryconvert, pyconvertarg, pyconvert_result using ..GC: GC -using Pkg: Pkg using Base: @propagate_inbounds, allocatedinline import ..Core: Py include("C.jl") include("base.jl") -include("raw.jl") include("any.jl") -include("iter.jl") -include("type.jl") -include("module.jl") include("io.jl") -include("number.jl") include("objectarray.jl") +include("collection.jl") include("array.jl") include("vector.jl") include("dict.jl") @@ -33,26 +28,20 @@ include("set.jl") include("callback.jl") function __init__() - Cjl.C.with_gil() do + Cjl.C.with_gil() do init_base() - init_raw() init_any() - init_iter() - init_type() - init_module() init_io() - init_number() + init_collection() init_array() init_vector() init_dict() init_set() - init_callback() # add packages to juliacall jl = pyjuliacallmodule jl.Core = Base.Core jl.Base = Base jl.Main = Main - jl.Pkg = Pkg jl.PythonCall = PythonCall end end diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl index 92a53a2b..6a521bae 100644 --- a/src/JlWrap/any.jl +++ b/src/JlWrap/any.jl @@ -1,20 +1,25 @@ const pyjlanytype = pynew() +const pyjlitertype = pynew() # pyjlany_repr(self) = Py("") function pyjlany_repr(self) - str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit=>true, :displaysize=>(23,80))) + str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit => true, :displaysize => (23, 80))) # type = self isa Function ? "Function" : self isa Type ? "Type" : nameof(typeof(self)) sep = '\n' in str ? '\n' : ' ' - Py("Julia:$sep$str") + Py("$(sep)$(str)"::String) end # Note: string(self) doesn't always return a String -pyjlany_str(self) = Py(sprint(print, self)) +pyjlany_str(self) = Py(sprint(print, self)::String) + +pyjl_attr_py2jl(k::String) = replace(k, r"_[b]+$" => (x -> "!"^(length(x) - 1))) + +pyjl_attr_jl2py(k::String) = replace(k, r"!+$" => (x -> "_" * "b"^length(x))) function pyjlany_getattr(self, k_::Py) k = Symbol(pyjl_attr_py2jl(pyconvert(String, k_))) pydel!(k_) - Py(getproperty(self, k)) + pyjl(getproperty(self, k)) end pyjl_handle_error_type(::typeof(pyjlany_getattr), self, exc) = pybuiltins.AttributeError @@ -27,15 +32,43 @@ function pyjlany_setattr(self, k_::Py, v_::Py) end pyjl_handle_error_type(::typeof(pyjlany_setattr), self, exc) = pybuiltins.AttributeError -pyjlany_dir(self) = pylist(pyjl_attr_jl2py(string(k)) for k in propertynames(self, true)) +function pyjlany_dir(self) + ks = Symbol[] + if self isa Module + append!(ks, names(self, all=true, imported=true)) + for m in ccall(:jl_module_usings, Any, (Any,), self)::Vector + append!(ks, names(m)) + end + else + append!(ks, propertynames(self, true)) + end + pylist(pyjl_attr_jl2py(string(k)) for k in ks) +end function pyjlany_call(self, args_::Py, kwargs_::Py) if pylen(kwargs_) > 0 args = pyconvert(Vector{Any}, args_) kwargs = pyconvert(Dict{Symbol,Any}, kwargs_) - ans = Py(self(args...; kwargs...)) + ans = pyjl(self(args...; kwargs...)) elseif pylen(args_) > 0 args = pyconvert(Vector{Any}, args_) + ans = pyjl(self(args...)) + else + ans = pyjl(self()) + end + pydel!(args_) + pydel!(kwargs_) + ans +end +pyjl_handle_error_type(::typeof(pyjlany_call), self, exc) = exc isa MethodError && exc.f === self ? pybuiltins.TypeError : PyNULL + +function pyjlany_callback(self, args_::Py, kwargs_::Py) + if pylen(kwargs_) > 0 + args = pyconvert(Vector{Py}, args_) + kwargs = pyconvert(Dict{Symbol,Py}, kwargs_) + ans = Py(self(args...; kwargs...)) + elseif pylen(args_) > 0 + args = pyconvert(Vector{Py}, args_) ans = Py(self(args...)) else ans = Py(self()) @@ -44,16 +77,27 @@ function pyjlany_call(self, args_::Py, kwargs_::Py) pydel!(kwargs_) ans end -pyjl_handle_error_type(::typeof(pyjlany_call), self, exc) = exc isa MethodError && exc.f === self ? pybuiltins.TypeError : PyNULL +pyjl_handle_error_type(::typeof(pyjlany_callback), self, exc::MethodError) = exc.f === self ? pybuiltins.TypeError : PyNULL function pyjlany_getitem(self, k_::Py) - if pyistuple(k_) - k = pyconvert(Vector{Any}, k_) - pydel!(k_) - Py(self[k...]) + if self isa Type + if pyistuple(k_) + k = pyconvert(Vector{Any}, k_) + pydel!(k_) + pyjl(self{k...}) + else + k = pyconvert(Any, k_) + pyjl(self{k}) + end else - k = pyconvert(Any, k_) - Py(self[k]) + if pyistuple(k_) + k = pyconvert(Vector{Any}, k_) + pydel!(k_) + pyjl(self[k...]) + else + k = pyconvert(Any, k_) + pyjl(self[k]) + end end end pyjl_handle_error_type(::typeof(pyjlany_getitem), self, exc) = exc isa BoundsError ? pybuiltins.IndexError : exc isa KeyError ? pybuiltins.KeyError : PyNULL @@ -85,61 +129,65 @@ function pyjlany_delitem(self, k_::Py) end pyjl_handle_error_type(::typeof(pyjlany_delitem), self, exc) = exc isa BoundsError ? pybuiltins.IndexError : exc isa KeyError ? pybuiltins.KeyError : PyNULL -pyjlany_contains(self, v::Py) = Py(@pyconvert(eltype(self), v, return Py(false)) in self) +pyjlany_contains(self, v::Py) = Py((@pyconvert(eltype(self), v, return Py(false)) in self)::Bool) pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = exc isa MethodError && exc.f === in ? pybuiltins.TypeError : PyNULL struct pyjlany_op{OP} - op :: OP + op::OP end -(op::pyjlany_op)(self) = Py(op.op(self)) +(op::pyjlany_op)(self) = pyjl(op.op(self)) function (op::pyjlany_op)(self, other_::Py) if pyisjl(other_) other = pyjlvalue(other_) pydel!(other_) - Py(op.op(self, other)) else - pybuiltins.NotImplemented + other = pyconvert(Any, other_) end + pyjl(op.op(self, other)) end function (op::pyjlany_op)(self, other_::Py, other2_::Py) - if pyisjl(other_) && pyisjl(other2_) + if pyisjl(other_) other = pyjlvalue(other_) - other2 = pyjlvalue(other2_) pydel!(other_) - pydel!(other2_) - Py(op.op(self, other, other2)) else - pybuiltins.NotImplemented + other = pyconvert(Any, other) end + if pyisjl(other2_) + other2 = pyjlvalue(other2_) + pydel!(other2_) + end + pyjl(op.op(self, other, other2)) end pyjl_handle_error_type(op::pyjlany_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL struct pyjlany_rev_op{OP} - op :: OP + op::OP end function (op::pyjlany_rev_op)(self, other_::Py) if pyisjl(other_) other = pyjlvalue(other_) pydel!(other_) - Py(op.op(other, self)) else - pybuiltins.NotImplemented + other = pyconvert(Any, other_) end + pyjl(op.op(other, self)) end function (op::pyjlany_rev_op)(self, other_::Py, other2_::Py) - if pyisjl(other_) && pyisjl(other2_) + if pyisjl(other_) other = pyjlvalue(other_) - other2 = pyjlvalue(other2_) pydel!(other_) - pydel!(other2_) - Py(op.op(other, self, other2)) else - pybuiltins.NotImplemented + other = pyconvert(Any, other) end + if pyisjl(other2_) + other2 = pyjlvalue(other2_) + pydel!(other2_) + end + pyjl(op.op(other, self, other2)) end pyjl_handle_error_type(op::pyjlany_rev_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL -pyjlany_name(self) = Py(string(nameof(self))) +pyjlany_name(self) = Py(string(nameof(self))::String) pyjl_handle_error_type(::typeof(pyjlany_name), self, exc) = exc isa MethodError && exc.f === nameof ? pybuiltins.AttributeError : PyNULL function pyjlany_display(self, mime_::Py) @@ -176,7 +224,7 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py) for m in mimes try io = IOBuffer() - show(IOContext(io, :limit=>true), MIME(m), self) + show(IOContext(io, :limit => true), MIME(m), self) v = take!(io) ans[m] = vo = istextmime(m) ? pystr(String(v)) : pybytes(v) pydel!(vo) @@ -187,22 +235,128 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py) return ans end +pyjlany_eval(self::Module, expr::Py) = pyjl(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr))))) +pyjl_handle_error_type(::typeof(pyjlany_eval), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_int(self) = pyint(convert(Integer, self)) +pyjl_handle_error_type(::typeof(pyjlany_int), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_float(self) = pyfloat(convert(AbstractFloat, self)) +pyjl_handle_error_type(::typeof(pyjlany_float), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_complex(self) = pycomplex(convert(Complex, self)) +pyjl_handle_error_type(::typeof(pyjlany_complex), self, exc::MethodError) = pybuiltins.TypeError + +function pyjlany_index(self) + if self isa Integer + pyint(self) + else + errset(pybuiltins.TypeError, "Only Julia 'Integer' values can be used as Python indices, not '$(typeof(self))'") + PyNULL + end +end + +function pyjlany_bool(self) + if self isa Bool + pybool(self) + else + errset(pybuiltins.TypeError, "Only Julia 'Bool' values can be tested for truthyness, not '$(typeof(self))'") + PyNULL + end +end + +pyjlany_trunc(self) = pyint(trunc(Integer, self)) +pyjl_handle_error_type(::typeof(pyjlany_trunc), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_floor(self) = pyint(floor(Integer, self)) +pyjl_handle_error_type(::typeof(pyjlany_floor), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_ceil(self) = pyint(ceil(Integer, self)) +pyjl_handle_error_type(::typeof(pyjlany_ceil), self, exc::MethodError) = pybuiltins.TypeError + +pyjlany_round(self) = pyint(round(Integer, self)) +function pyjlany_round(self, ndigits_::Py) + ndigits = pyconvertarg(Int, ndigits_, "ndigits") + pydel!(ndigits_) + pyjl(round(self; digits=ndigits)) +end +pyjl_handle_error_type(::typeof(pyjlany_round), self, exc::MethodError) = pybuiltins.TypeError + +mutable struct Iterator + value::Any + state::Any + started::Bool + finished::Bool +end + +Iterator(x) = Iterator(x, nothing, false, false) +Iterator(x::Iterator) = x + +function Base.iterate(x::Iterator, ::Nothing=nothing) + if x.finished + s = nothing + elseif x.started + s = iterate(x.value, x.state) + else + s = iterate(x.value) + end + if s === nothing + x.finished = true + nothing + else + x.started = true + x.state = s[2] + (s[1], nothing) + end +end + +function pyjlany_next(self) + s = iterate(self) + if s === nothing + errset(pybuiltins.StopIteration) + PyNULL + else + pyjl(s[1]) + end +end + +function pyjliter_next(self) + s = iterate(self) + if s === nothing + errset(pybuiltins.StopIteration) + PyNULL + else + Py(s[1]) + end +end + +pyjlany_hash(self) = pyint(hash(self)) + function init_any() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class AnyValue(ValueBase): + class JlBase2(JlBase): __slots__ = () def __repr__(self): - if self._jl_isnull(): - return "" + t = type(self) + if t is Jl: + name = "Julia" else: - return self._jl_callmethod($(pyjl_methodnum(pyjlany_repr))) + name = t.__name__ + return name + ":" + self._jl_callmethod($(pyjl_methodnum(pyjlany_repr))) + class JlIter(JlBase2): + __slots__ = () + def __iter__(self): + return self + def __hash__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash))) + def __next__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjliter_next))) + class Jl(JlBase2): + __slots__ = () def __str__(self): - if self._jl_isnull(): - return "NULL" - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlany_str))) + return self._jl_callmethod($(pyjl_methodnum(pyjlany_str))) def __getattr__(self, k): if k.startswith("__") and k.endswith("__"): raise AttributeError(k) @@ -210,7 +364,7 @@ function init_any() return self._jl_callmethod($(pyjl_methodnum(pyjlany_getattr)), k) def __setattr__(self, k, v): try: - ValueBase.__setattr__(self, k, v) + JlBase.__setattr__(self, k, v) except AttributeError: if k.startswith("__") and k.endswith("__"): raise @@ -218,11 +372,11 @@ function init_any() return self._jl_callmethod($(pyjl_methodnum(pyjlany_setattr)), k, v) def __dir__(self): - return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlany_dir))) + return JlBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlany_dir))) def __call__(self, *args, **kwargs): return self._jl_callmethod($(pyjl_methodnum(pyjlany_call)), args, kwargs) def __bool__(self): - return True + return self._jl_callmethod($(pyjl_methodnum(pyjlany_bool))) def __len__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(length)))) def __getitem__(self, k): @@ -233,6 +387,8 @@ function init_any() self._jl_callmethod($(pyjl_methodnum(pyjlany_delitem)), k) def __iter__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(Iterator)))) + def __next__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_next))) def __reversed__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(reverse)))) def __contains__(self, v): @@ -243,6 +399,8 @@ function init_any() return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(-)))) def __abs__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(abs)))) + def abs(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(abs)))) def __invert__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(~)))) def __add__(self, other): @@ -299,6 +457,8 @@ function init_any() return self._jl_callmethod($(pyjl_methodnum(pyjlany_rev_op(⊻))), other) def __ror__(self, other): return self._jl_callmethod($(pyjl_methodnum(pyjlany_rev_op(|))), other) + def __hash__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash))) def __eq__(self, other): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(==))), other) def __ne__(self, other): @@ -311,46 +471,51 @@ function init_any() return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(≥))), other) def __gt__(self, other): return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(>))), other) - def __hash__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(hash)))) + def __int__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_int))) + def __float__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_float))) + def __complex__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_complex))) + def __index__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_index))) + def __trunc__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_trunc))) + def __floor__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_floor))) + def __ceil__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_ceil))) + def __round__(self, ndigits=None): + if ndigits is None: + return self._jl_callmethod($(pyjl_methodnum(pyjlany_round))) + else: + return self._jl_callmethod($(pyjl_methodnum(pyjlany_round)), ndigits) @property def __name__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlany_name))) - def _jl_raw(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlraw))) - def _jl_display(self, mime=None): + def jl_display(self, mime=None): return self._jl_callmethod($(pyjl_methodnum(pyjlany_display)), mime) - def _jl_help(self, mime=None): + def jl_help(self, mime=None): return self._jl_callmethod($(pyjl_methodnum(pyjlany_help)), mime) + def jl_eval(self, expr): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_eval)), expr) + def jl_to_py(self): + return self._jl_callmethod($(pyjl_methodnum(Py))) + def jl_callback(self, *args, **kwargs): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_callback)), args, kwargs) def _repr_mimebundle_(self, include=None, exclude=None): return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude) """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlanytype, jl.AnyValue) + pycopy!(pyjlanytype, jl.Jl) + pycopy!(pyjlitertype, jl.JlIter) end """ - pyjl([t=pyjltype(x)], x) - -Create a Python object wrapping the Julia object `x`. - -If `x` is mutable, then mutating the returned object also mutates `x`, and vice versa. - -Its Python type is normally inferred from the type of `x`, but can be specified with `t`. + pyjl(x) -For example if `x` is an `AbstractVector` then the object will have type `juliacall.VectorValue`. -This object will satisfy the Python sequence interface, so for example uses 0-up indexing. - -To define a custom conversion for your type `T`, overload `pyjltype(::T)`. +Create a Python `juliacall.Jl` object wrapping the Julia object `x`. """ -pyjl(v) = pyjl(pyjltype(v), v) +pyjl(v) = pyjl(pyjlanytype, v) export pyjl -""" - pyjltype(x) - -The subtype of `juliacall.AnyValue` which the Julia object `x` is wrapped as by `pyjl(x)`. - -Overload `pyjltype(::T)` to define a custom conversion for your type `T`. -""" -pyjltype(::Any) = pyjlanytype -export pyjltype +pyjliter(x) = pyjl(pyjlitertype, x) diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl index 2cb33bbc..fa52560b 100644 --- a/src/JlWrap/array.jl +++ b/src/JlWrap/array.jl @@ -63,7 +63,7 @@ function pyjl_getarrayindices(x::AbstractArray{T,N}, ks::Py) where {T,N} if pyistuple(ks) if pylen(ks) == N return ntuple(N) do i - k = pytuple_getitem(ks, i-1) + k = pytuple_getitem(ks, i - 1) ans = pyjl_getaxisindex(axes(x, i), k) pydel!(k) return ans @@ -149,12 +149,12 @@ pyjlarray_isbufferabletype(::Type{NamedTuple{names,T}}) where {names,T} = function pyjlarray_buffer_info(x::AbstractArray{T,N}) where {T,N} if pyjlarray_isbufferabletype(T) Cjl.PyBufferInfo{N}( - ptr = Base.unsafe_convert(Ptr{T}, x), - readonly = !Utils.ismutablearray(x), - itemsize = sizeof(T), - format = pybufferformat(T), - shape = size(x), - strides = strides(x) .* Base.aligned_sizeof(T), + ptr=Base.unsafe_convert(Ptr{T}, x), + readonly=!Utils.ismutablearray(x), + itemsize=sizeof(T), + format=pybufferformat(T), + shape=size(x), + strides=strides(x) .* Base.aligned_sizeof(T), ) else error("element type is not bufferable") @@ -163,43 +163,44 @@ end const PYBUFFERFORMAT = IdDict{Type,String}() -pybufferformat(::Type{T}) where {T} = get!(PYBUFFERFORMAT, T) do - T == Cchar ? "b" : - T == Cuchar ? "B" : - T == Cshort ? "h" : - T == Cushort ? "H" : - T == Cint ? "i" : - T == Cuint ? "I" : - T == Clong ? "l" : - T == Culong ? "L" : - T == Clonglong ? "q" : - T == Culonglong ? "Q" : - T == Float16 ? "e" : - T == Cfloat ? "f" : - T == Cdouble ? "d" : - T == Complex{Float16} ? "Ze" : - T == Complex{Cfloat} ? "Zf" : - T == Complex{Cdouble} ? "Zd" : - T == Bool ? "?" : - T == Ptr{Cvoid} ? "P" : - if isstructtype(T) && isconcretetype(T) && allocatedinline(T) - n = fieldcount(T) - flds = [] - for i = 1:n - nm = fieldname(T, i) - tp = fieldtype(T, i) - push!(flds, string(pybufferformat(tp), nm isa Symbol ? ":$nm:" : "")) - d = - (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - - (fieldoffset(T, i) + sizeof(tp)) - @assert d ≥ 0 - d > 0 && push!(flds, "$(d)x") +pybufferformat(::Type{T}) where {T} = + get!(PYBUFFERFORMAT, T) do + T == Cchar ? "b" : + T == Cuchar ? "B" : + T == Cshort ? "h" : + T == Cushort ? "H" : + T == Cint ? "i" : + T == Cuint ? "I" : + T == Clong ? "l" : + T == Culong ? "L" : + T == Clonglong ? "q" : + T == Culonglong ? "Q" : + T == Float16 ? "e" : + T == Cfloat ? "f" : + T == Cdouble ? "d" : + T == Complex{Float16} ? "Ze" : + T == Complex{Cfloat} ? "Zf" : + T == Complex{Cdouble} ? "Zd" : + T == Bool ? "?" : + T == Ptr{Cvoid} ? "P" : + if isstructtype(T) && isconcretetype(T) && allocatedinline(T) + n = fieldcount(T) + flds = [] + for i = 1:n + nm = fieldname(T, i) + tp = fieldtype(T, i) + push!(flds, string(pybufferformat(tp), nm isa Symbol ? ":$nm:" : "")) + d = + (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - + (fieldoffset(T, i) + sizeof(tp)) + @assert d ≥ 0 + d > 0 && push!(flds, "$(d)x") + end + string("T{", join(flds, " "), "}") + else + "$(sizeof(T))x" end - string("T{", join(flds, " "), "}") - else - "$(sizeof(T))x" end -end pyjlarray_isarrayabletype(::Type{T}) where {T} = T in ( UInt8, @@ -227,47 +228,48 @@ pyjlarray_isarrayabletype(::Type{NamedTuple{names,types}}) where {names,types} = const PYTYPESTRDESCR = IdDict{Type,Tuple{String,Py}}() -pytypestrdescr(::Type{T}) where {T} = get!(PYTYPESTRDESCR, T) do - c = Utils.islittleendian() ? '<' : '>' - T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) : - T == Int8 ? ("$(c)i1", PyNULL) : - T == UInt8 ? ("$(c)u1", PyNULL) : - T == Int16 ? ("$(c)i2", PyNULL) : - T == UInt16 ? ("$(c)u2", PyNULL) : - T == Int32 ? ("$(c)i4", PyNULL) : - T == UInt32 ? ("$(c)u4", PyNULL) : - T == Int64 ? ("$(c)i8", PyNULL) : - T == UInt64 ? ("$(c)u8", PyNULL) : - T == Float16 ? ("$(c)f2", PyNULL) : - T == Float32 ? ("$(c)f4", PyNULL) : - T == Float64 ? ("$(c)f8", PyNULL) : - T == Complex{Float16} ? ("$(c)c4", PyNULL) : - T == Complex{Float32} ? ("$(c)c8", PyNULL) : - T == Complex{Float64} ? ("$(c)c16", PyNULL) : - if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T) - n = fieldcount(T) - flds = [] - for i = 1:n - nm = fieldname(T, i) - tp = fieldtype(T, i) - ts, ds = pytypestrdescr(tp) - isempty(ts) && return ("", PyNULL) - push!( - flds, - (nm isa Integer ? "f$(nm-1)" : string(nm), pyisnull(ds) ? ts : ds), - ) - d = (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - (fieldoffset(T, i) + sizeof(tp)) - @assert d ≥ 0 - d > 0 && push!(flds, ("", "|V$(d)")) +pytypestrdescr(::Type{T}) where {T} = + get!(PYTYPESTRDESCR, T) do + c = Utils.islittleendian() ? '<' : '>' + T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) : + T == Int8 ? ("$(c)i1", PyNULL) : + T == UInt8 ? ("$(c)u1", PyNULL) : + T == Int16 ? ("$(c)i2", PyNULL) : + T == UInt16 ? ("$(c)u2", PyNULL) : + T == Int32 ? ("$(c)i4", PyNULL) : + T == UInt32 ? ("$(c)u4", PyNULL) : + T == Int64 ? ("$(c)i8", PyNULL) : + T == UInt64 ? ("$(c)u8", PyNULL) : + T == Float16 ? ("$(c)f2", PyNULL) : + T == Float32 ? ("$(c)f4", PyNULL) : + T == Float64 ? ("$(c)f8", PyNULL) : + T == Complex{Float16} ? ("$(c)c4", PyNULL) : + T == Complex{Float32} ? ("$(c)c8", PyNULL) : + T == Complex{Float64} ? ("$(c)c16", PyNULL) : + if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T) + n = fieldcount(T) + flds = [] + for i = 1:n + nm = fieldname(T, i) + tp = fieldtype(T, i) + ts, ds = pytypestrdescr(tp) + isempty(ts) && return ("", PyNULL) + push!( + flds, + (nm isa Integer ? "f$(nm-1)" : string(nm), pyisnull(ds) ? ts : ds), + ) + d = (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - (fieldoffset(T, i) + sizeof(tp)) + @assert d ≥ 0 + d > 0 && push!(flds, ("", "|V$(d)")) + end + ("|$(sizeof(T))V", pylist(flds)) + else + ("", PyNULL) end - ("|$(sizeof(T))V", pylist(flds)) - else - ("", PyNULL) end -end -pyjlarray_array__array(x::AbstractArray) = x isa Array ? Py(nothing) : pyjl(Array(x)) -pyjlarray_array__pyobjectarray(x::AbstractArray) = pyjl(PyObjectArray(x)) +pyjlarray_array__array(x::AbstractArray) = x isa Array ? Py(nothing) : pyjlarray(Array(x)) +pyjlarray_array__pyobjectarray(x::AbstractArray) = pyjlarray(PyObjectArray(x)) function pyjlarray_array_interface(x::AbstractArray{T,N}) where {T,N} if pyjlarray_isarrayabletype(eltype(x)) @@ -295,21 +297,19 @@ function init_array() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class ArrayValue(AnyValue): + class JlArray(JlCollection): __slots__ = () _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info)) + def __init__(self, value): + JlBase.__init__(self, value, Base.AbstractArray) @property def ndim(self): return self._jl_callmethod($(pyjl_methodnum(Py ∘ ndims))) @property def shape(self): return self._jl_callmethod($(pyjl_methodnum(Py ∘ size))) - def copy(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy))) def reshape(self, shape): return self._jl_callmethod($(pyjl_methodnum(pyjlarray_reshape)), shape) - def __bool__(self): - return bool(len(self)) def __getitem__(self, k): return self._jl_callmethod($(pyjl_methodnum(pyjlarray_getitem)), k) def __setitem__(self, k, v): @@ -339,7 +339,20 @@ function init_array() import numpy return numpy.array(self, dtype=dtype, copy=copy, order=order) """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlarraytype, jl.ArrayValue) + pycopy!(pyjlarraytype, jl.JlArray) end -pyjltype(::AbstractArray) = pyjlarraytype +""" + pyjlarray(x::AbstractArray) + +Wrap `x` as a Python array-like object. + +This object can be converted to a Numpy array with `numpy.array(v)`, `v.to_numpy()` or +`v.__array__()` and supports these Numpy attributes: `ndim`, `shape`, `copy`, `reshape`. + +If `x` is one-dimensional (an `AbstractVector`) then it also behaves as a `list`. +""" +pyjlarray(x::AbstractArray) = pyjl(pyjlarraytype, x) +export pyjlarray + +Py(x::AbstractArray) = pyjlarray(x) diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl index 08793e63..9b61abef 100644 --- a/src/JlWrap/base.jl +++ b/src/JlWrap/base.jl @@ -9,50 +9,32 @@ pyjl(t, v) = pynew(errcheck(@autopy t Cjl.PyJuliaValue_New(getptr(t_), v))) """ pyisjl(x) -Test whether `x` is a wrapped Julia value, namely an instance of `juliacall.ValueBase`. +Test whether `x` is a wrapped Julia value, namely an instance of `juliacall.JlBase`. """ pyisjl(x) = pytypecheck(x, pyjlbasetype) export pyisjl -pyjlisnull(x) = @autopy x begin - if pyisjl(x_) - Cjl.PyJuliaValue_IsNull(getptr(x_)) - else - error("Expecting a 'juliacall.ValueBase', got a '$(pytype(x_).__name__)'") - end -end - """ pyjlvalue(x) Extract the value from the wrapped Julia value `x`. """ -pyjlvalue(x) = @autopy x begin - if pyjlisnull(x_) - error("Julia value is NULL") - else - _pyjl_getvalue(x_) - end -end +pyjlvalue(x) = @autopy x _pyjl_getvalue(x_) export pyjlvalue function init_base() setptr!(pyjlbasetype, incref(Cjl.PyJuliaBase_Type[])) - pyjuliacallmodule.ValueBase = pyjlbasetype + pyjuliacallmodule.JlBase = pyjlbasetype # conversion rule priority = PYCONVERT_PRIORITY_WRAP - pyconvert_add_rule("juliacall:ValueBase", Any, pyconvert_rule_jlvalue, priority) + pyconvert_add_rule("juliacall:JlBase", Any, pyconvert_rule_jlvalue, priority) end pyconvert_rule_jlvalue(::Type{T}, x::Py) where {T} = pyconvert_tryconvert(T, _pyjl_getvalue(x)) function Cjl._pyjl_callmethod(f, self_::C.PyPtr, args_::C.PyPtr, nargs::C.Py_ssize_t) @nospecialize f - if Cjl.PyJuliaValue_IsNull(self_) - errset(pybuiltins.TypeError, "Julia object is NULL") - return C.PyNULL - end in_f = false self = Cjl.PyJuliaValue_GetValue(self_) try @@ -91,7 +73,7 @@ function Cjl._pyjl_callmethod(f, self_::C.PyPtr, args_::C.PyPtr, nargs::C.Py_ssi if in_f return pyjl_handle_error(f, self, exc) else - errset(pyJuliaError, pytuple((pyjlraw(exc), pyjlraw(catch_backtrace())))) + errset(pyJuliaError, pytuple((pyjl(exc), pyjl(catch_backtrace())))) return C.PyNULL end catch @@ -107,7 +89,7 @@ function pyjl_handle_error(f, self, exc) t = pyjl_handle_error_type(f, self, exc)::Py if pyisnull(t) # NULL => raise JuliaError - errset(pyJuliaError, pytuple((pyjlraw(exc), pyjlraw(catch_backtrace())))) + errset(pyJuliaError, pytuple((pyjl(exc), pyjl(catch_backtrace())))) return C.PyNULL elseif pyistype(t) # Exception type => raise this type of error diff --git a/src/JlWrap/callback.jl b/src/JlWrap/callback.jl index c25c10dd..4b7fcb57 100644 --- a/src/JlWrap/callback.jl +++ b/src/JlWrap/callback.jl @@ -1,64 +1,6 @@ const pywrapcallback = pynew() -const pyjlcallbacktype = pynew() -pyjlcallback_repr(self) = Py("") - -pyjlcallback_str(self) = Py(sprint(print, self)) - -function pyjlcallback_call(self, args_::Py, kwargs_::Py) - if pylen(kwargs_) > 0 - args = pyconvert(Vector{Py}, args_) - kwargs = pyconvert(Dict{Symbol,Py}, kwargs_) - ans = Py(self(args...; kwargs...)) - elseif (nargs = pylen(args_)) > 0 - args = pyconvert(Vector{Py}, args_) - @assert length(args) == nargs - if nargs == 1 - ans = Py(self(args[1])) - elseif nargs == 2 - ans = Py(self(args[1], args[2])) - elseif nargs == 3 - ans = Py(self(args[1], args[2], args[3])) - elseif nargs == 4 - ans = Py(self(args[1], args[2], args[3], args[4])) - elseif nargs == 5 - ans = Py(self(args[1], args[2], args[3], args[4], args[5])) - else - ans = Py(self(args...)) - end - else - ans = Py(self()) - end - pydel!(args_) - pydel!(kwargs_) - ans -end -pyjl_handle_error_type(::typeof(pyjlcallback_call), self, exc::MethodError) = exc.f === self ? pybuiltins.TypeError : PyNULL - -function init_callback() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class CallbackValue(ValueBase): - __slots__ = () - def __repr__(self): - if self._jl_isnull(): - return "" - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlcallback_repr))) - def __str__(self): - if self._jl_isnull(): - return "NULL" - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlcallback_str))) - def __call__(self, *args, **kwargs): - return self._jl_callmethod($(pyjl_methodnum(pyjlcallback_call)), args, kwargs) - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlcallbacktype, jl.CallbackValue) - pycopy!(pywrapcallback, pybuiltins.eval("lambda f: lambda *args, **kwargs: f(*args, **kwargs)", pydict())) -end - -pyjlcallback(f) = pyjl(pyjlcallbacktype, f) +pyjlcallback(f) = pyjl(f).jl_callback """ pyfunc(f; [name], [qualname], [doc], [signature]) @@ -66,25 +8,33 @@ pyjlcallback(f) = pyjl(pyjlcallbacktype, f) Wrap the callable `f` as an ordinary Python function. The name, qualname, docstring or signature can optionally be set with `name`, `qualname`, -`doc` or `signature`. +`doc` or `signature`. If either of `name` or `qualname` are given, the other is inferred. Unlike `Py(f)` (or `pyjl(f)`), the arguments passed to `f` are always of type `Py`, i.e. they are never converted. """ -function pyfunc(f; name=nothing, qualname=name, doc=nothing, signature=nothing, wrap=pywrapcallback) +function pyfunc(f; name=nothing, qualname=nothing, doc=nothing, signature=nothing, wrap=pywrapcallback) f2 = ispy(f) ? f : pyjlcallback(f) if wrap isa Pair wrapargs, wrapfunc = wrap else wrapargs, wrapfunc = (), wrap end + if wrapfunc === pywrapcallback && pyisnull(pywrapcallback) + pycopy!(pywrapcallback, pybuiltins.eval("lambda f: lambda *args, **kwargs: f(*args, **kwargs)", pydict())) + end if wrapfunc isa AbstractString f3 = pybuiltins.eval(wrapfunc, pydict())(f2, wrapargs...) else f3 = wrapfunc(f2, wrapargs...) end + if name === nothing && qualname !== nothing + name = split(qualname, '.')[end] + elseif name !== nothing && qualname === nothing + qualname = name + end f3.__name__ = name === nothing ? "" : name - f3.__qualname__ = name === nothing ? "" : qualname + f3.__qualname__ = qualname === nothing ? "" : qualname if doc !== nothing f3.__doc__ = doc end @@ -120,8 +70,8 @@ pystaticmethod(f; kw...) = pybuiltins.staticmethod(ispy(f) ? f : pyfunc(f; kw... export pystaticmethod """ - pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing) - pyproperty(get) + pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing, ...) + pyproperty(get, set=nothing, del=nothing; doc=nothing, ...) Create a Python `property` with the given getter, setter and deleter. @@ -129,12 +79,12 @@ If `get`, `set` or `del` is not a Python object (e.g. if it is a `Function`) the converted to one with [`pyfunc`](@ref PythonCall.pyfunc). In particular this means the arguments passed to it are always of type `Py`. """ -pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing) = +pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing, kw...) = pybuiltins.property( - fget = ispy(get) || get === nothing ? get : pyfunc(get), - fset = ispy(set) || set === nothing ? set : pyfunc(set), - fdel = ispy(del) || del === nothing ? del : pyfunc(del), + fget = ispy(get) || get === nothing ? get : pyfunc(get; kw...), + fset = ispy(set) || set === nothing ? set : pyfunc(set; kw...), + fdel = ispy(del) || del === nothing ? del : pyfunc(del; kw...), doc = doc, ) -pyproperty(get) = pyproperty(get=get) +pyproperty(get, set=nothing, del=nothing; doc=nothing, kw...) = pyproperty(; get=get, set=set, del=del, doc=doc, kw...) export pyproperty diff --git a/src/JlWrap/collection.jl b/src/JlWrap/collection.jl new file mode 100644 index 00000000..d740597e --- /dev/null +++ b/src/JlWrap/collection.jl @@ -0,0 +1,57 @@ +const pyjlcollectiontype = pynew() + +pyjlcollection_clear(x) = (empty!(x); Py(nothing)) + +pyjlcollection_contains(x, v::Py) = pybool(in(@pyconvert(eltype(x), v, (return Py(false))), x)::Bool) + +pyjlcollection_eq(self, other) = pybool((self == pyjlvalue(other))::Bool) + +function init_collection() + jl = pyjuliacallmodule + pybuiltins.exec(pybuiltins.compile(""" + $("\n"^(@__LINE__()-1)) + class JlCollection(JlBase2): + __slots__ = () + def __len__(self): + return self._jl_callmethod($(pyjl_methodnum(pyint ∘ length))) + def __bool__(self): + return self._jl_callmethod($(pyjl_methodnum(pybool ∘ !isempty))) + def __iter__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator))) + def __hash__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash))) + def __eq__(self, other): + if isinstance(self, type(other)) or isinstance(other, type(self)): + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection_eq)), other) + else: + return NotImplemented + def __contains__(self, v): + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection_contains)), v) + def copy(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection ∘ copy))) + def clear(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection_clear))) + import collections.abc + collections.abc.Collection.register(JlCollection) + del collections + """, @__FILE__(), "exec"), jl.__dict__) + pycopy!(pyjlcollectiontype, jl.JlCollection) +end + +""" + pyjlcollection(x) + +Wrap `x` as a Python `collections.abc.Collection` object. + +The argument should be a collection of values, in the sense of supporting `iterate`, +`hash`, `in` and `length`. This includes `AbstractArray`, `AbstractSet`, `AbstractDict`, +`Tuple`, `Base.RefValue` (`Ref(...)`) and `Base.ValueIterator` (`values(Dict(...))`). +""" +pyjlcollection(x) = pyjl(pyjlcollectiontype, x) +pyjlcollection(x::AbstractSet) = pyjlset(x) +pyjlcollection(x::AbstractArray) = pyjlarray(x) +pyjlcollection(x::AbstractDict) = pyjldict(x) +export pyjlcollection + +Py(x::Base.ValueIterator) = pyjlcollection(x) +Py(x::Base.RefValue) = pyjlcollection(x) diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl index 4d01674a..b6c7abbb 100644 --- a/src/JlWrap/dict.jl +++ b/src/JlWrap/dict.jl @@ -9,12 +9,8 @@ Base.iterate(x::DictPairSet, st) = Base.in(v::Pair, x::DictPairSet) = v in x.dict Base.in(v::Tuple{Any,Any}, x::DictPairSet) = Pair(v[1], v[2]) in x.dict -pyjldict_iter(x::AbstractDict) = Py(Iterator(keys(x))) - pyjldict_contains(x::AbstractDict, k::Py) = Py(haskey(x, @pyconvert(keytype(x), k, return Py(false)))) -pyjldict_clear(x::AbstractDict) = (empty!(x); Py(nothing)) - pyjldict_getitem(x::AbstractDict, k::Py) = Py(x[pyconvert(keytype(x), k)]) pyjldict_setitem(x::AbstractDict, k::Py, v::Py) = (x[pyconvertarg(keytype(x), k, "key")] = pyconvertarg(valtype(x), v, "value"); Py(nothing)) @@ -23,7 +19,7 @@ pyjldict_delitem(x::AbstractDict, k::Py) = (delete!(x, pyconvert(keytype(x), k)) function pyjldict_update(x::AbstractDict, items_::Py) for item_ in items_ - (k, v) = pyconvert(Tuple{keytype(x), valtype(x)}, item_) + (k, v) = pyconvert(Tuple{keytype(x),valtype(x)}, item_) x[k] = v end Py(nothing) @@ -35,13 +31,15 @@ function init_dict() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class DictValue(AnyValue): + class JlDict(JlCollection): __slots__ = () _jl_undefined_ = object() - def __bool__(self): - return bool(len(self)) + def __init__(self, value=None): + if value is None: + value = Base.Dict() + JlBase.__init__(self, value, Base.AbstractDict) def __iter__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjldict_iter))) + return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator ∘ keys))) def __contains__(self, key): return self._jl_callmethod($(pyjl_methodnum(pyjldict_contains)), key) def __getitem__(self, key): @@ -57,11 +55,11 @@ function init_dict() else: raise KeyError(key) def keys(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ keys))) + return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ keys))) def values(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ values))) + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection ∘ values))) def items(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ DictPairSet))) + return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ DictPairSet))) def get(self, key, default=None): if key in self: return self[key] @@ -71,8 +69,6 @@ function init_dict() if key not in self: self[key] = default return self[key] - def clear(self): - return self._jl_callmethod($(pyjl_methodnum(pyjldict_clear))) def pop(self, key, default=_jl_undefined_): if key in self: ans = self[key] @@ -98,13 +94,19 @@ function init_dict() self._jl_callmethod($(pyjl_methodnum(pyjldict_update)), items) if kwargs: self.update(kwargs) - def copy(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy))) import collections.abc - collections.abc.MutableMapping.register(DictValue) + collections.abc.MutableMapping.register(JlDict) del collections """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjldicttype, jl.DictValue) + pycopy!(pyjldicttype, jl.JlDict) end -pyjltype(::AbstractDict) = pyjldicttype +""" + pyjldict(x::AbstractDict) + +Wrap `x` as a Python `dict`-like object. +""" +pyjldict(x::AbstractDict) = pyjl(pyjldicttype, x) +export pyjldict + +Py(x::AbstractDict) = pyjldict(x) diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl index 85785fee..a1662f22 100644 --- a/src/JlWrap/io.jl +++ b/src/JlWrap/io.jl @@ -205,8 +205,12 @@ function init_io() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class IOValueBase(AnyValue): + class JlIOBase(JlBase2): __slots__ = () + def __init__(self, value): + JlBase.__init__(self, value, Base.IO) + def __hash__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash))) def close(self): return self._jl_callmethod($(pyjl_methodnum(pyjlio_close))) @property @@ -256,7 +260,7 @@ function init_io() return line else: raise StopIteration - class BinaryIOValue(IOValueBase): + class JlBinaryIO(JlIOBase): __slots__ = () def detach(self): raise ValueError("Cannot detach '{}'.".format(type(self))) @@ -272,7 +276,7 @@ function init_io() return self.readinto(b) def write(self, b): return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_write)), b) - class TextIOValue(IOValueBase): + class JlTextIO(JlIOBase): __slots__ = () @property def encoding(self): @@ -289,14 +293,14 @@ function init_io() def write(self, s): return self._jl_callmethod($(pyjl_methodnum(pyjltextio_write)), s) import io - io.IOBase.register(IOValueBase) - io.BufferedIOBase.register(BinaryIOValue) - io.TextIOBase.register(TextIOValue) + io.IOBase.register(JlIOBase) + io.BufferedIOBase.register(JlBinaryIO) + io.TextIOBase.register(JlTextIO) del io """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjliobasetype, jl.IOValueBase) - pycopy!(pyjlbinaryiotype, jl.BinaryIOValue) - pycopy!(pyjltextiotype, jl.TextIOValue) + pycopy!(pyjliobasetype, jl.JlIOBase) + pycopy!(pyjlbinaryiotype, jl.JlBinaryIO) + pycopy!(pyjltextiotype, jl.JlTextIO) end pyiobase(v::IO) = pyjl(pyjliobasetype, v) @@ -319,4 +323,4 @@ Wrap `io` as a Python text IO object. pytextio(v::IO) = pyjl(pyjltextiotype, v) export pytextio -pyjltype(::IO) = pyjlbinaryiotype +Py(x::IO) = pybinaryio(x) diff --git a/src/JlWrap/iter.jl b/src/JlWrap/iter.jl deleted file mode 100644 index 90d24537..00000000 --- a/src/JlWrap/iter.jl +++ /dev/null @@ -1,42 +0,0 @@ -mutable struct Iterator - val::Any - st::Any -end -Iterator(x) = Iterator(x, nothing) -Base.length(x::Iterator) = length(x.val) - -const pyjlitertype = pynew() - -function pyjliter_next(self::Iterator) - val = self.val - st = self.st - if st === nothing - z = iterate(val) - else - z = iterate(val, something(st)) - end - if z === nothing - errset(pybuiltins.StopIteration) - PyNULL - else - r, newst = z - self.st = Some(newst) - Py(r) - end -end - -function init_iter() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class IteratorValue(AnyValue): - __slots__ = () - def __iter__(self): - return self - def __next__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjliter_next))) - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlitertype, jl.IteratorValue) -end - -pyjltype(::Iterator) = pyjlitertype diff --git a/src/JlWrap/module.jl b/src/JlWrap/module.jl deleted file mode 100644 index 8d879ae5..00000000 --- a/src/JlWrap/module.jl +++ /dev/null @@ -1,30 +0,0 @@ -const pyjlmoduletype = pynew() - -function pyjlmodule_dir(self::Module) - ks = Symbol[] - append!(ks, names(self, all = true, imported = true)) - for m in ccall(:jl_module_usings, Any, (Any,), self)::Vector - append!(ks, names(m)) - end - pylist(pyjl_attr_jl2py(string(k)) for k in ks) -end - -function pyjlmodule_seval(self::Module, expr::Py) - Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr))))) -end - -function init_module() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class ModuleValue(AnyValue): - __slots__ = () - def __dir__(self): - return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlmodule_dir))) - def seval(self, expr): - return self._jl_callmethod($(pyjl_methodnum(pyjlmodule_seval)), expr) - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlmoduletype, jl.ModuleValue) -end - -pyjltype(::Module) = pyjlmoduletype diff --git a/src/JlWrap/number.jl b/src/JlWrap/number.jl deleted file mode 100644 index 1ed4013c..00000000 --- a/src/JlWrap/number.jl +++ /dev/null @@ -1,233 +0,0 @@ -const pyjlnumbertype = pynew() -const pyjlcomplextype = pynew() -const pyjlrealtype = pynew() -const pyjlrationaltype = pynew() -const pyjlintegertype = pynew() - -struct pyjlnumber_op{OP} - op :: OP -end -(op::pyjlnumber_op)(self) = Py(op.op(self)) -function (op::pyjlnumber_op)(self, other_::Py) - if pyisjl(other_) - other = pyjlvalue(other_) - pydel!(other_) - else - other = @pyconvert(Number, other_, return pybuiltins.NotImplemented) - end - Py(op.op(self, other)) -end -function (op::pyjlnumber_op)(self, other_::Py, other2_::Py) - if pyisjl(other_) - other = pyjlvalue(other_) - pydel!(other_) - else - other = @pyconvert(Number, other_, return pybuiltins.NotImplemented) - end - if pyisjl(other2_) - other2 = pyjlvalue(other2_) - pydel!(other2_) - else - other2 = @pyconvert(Number, other2_, return pybuiltins.NotImplemented) - end - Py(op.op(self, other, other2)) -end -pyjl_handle_error_type(op::pyjlnumber_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL - -struct pyjlnumber_rev_op{OP} - op :: OP -end -function (op::pyjlnumber_rev_op)(self, other_::Py) - if pyisjl(other_) - other = pyjlvalue(other_) - pydel!(other_) - else - other = @pyconvert(Number, other_, return pybuiltins.NotImplemented) - end - Py(op.op(other, self)) -end -function (op::pyjlnumber_rev_op)(self, other_::Py, other2_::Py) - if pyisjl(other_) - other = pyjlvalue(other_) - pydel!(other_) - else - other = @pyconvert(Number, other_, return pybuiltins.NotImplemented) - end - if pyisjl(other2_) - other2 = pyjlvalue(other2_) - pydel!(other2_) - else - other2 = @pyconvert(Number, other2_, return pybuiltins.NotImplemented) - end - Py(op.op(other, self, other2)) -end -pyjl_handle_error_type(op::pyjlnumber_rev_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL - -pyjlreal_trunc(self::Real) = Py(trunc(Integer, self)) -pyjl_handle_error_type(::typeof(pyjlreal_trunc), self, exc::MethodError) = exc.f === trunc ? pybuiltins.TypeError : PyNULL - -pyjlreal_floor(self::Real) = Py(floor(Integer, self)) -pyjl_handle_error_type(::typeof(pyjlreal_floor), self, exc::MethodError) = exc.f === floor ? pybuiltins.TypeError : PyNULL - -pyjlreal_ceil(self::Real) = Py(ceil(Integer, self)) -pyjl_handle_error_type(::typeof(pyjlreal_ceil), self, exc::MethodError) = exc.f === ceil ? pybuiltins.TypeError : PyNULL - -function pyjlreal_round(self::Real, ndigits_::Py) - ndigits = pyconvertarg(Union{Int,Nothing}, ndigits_, "ndigits") - pydel!(ndigits_) - if ndigits === nothing - Py(round(Integer, self)) - else - Py(round(self; digits = ndigits)) - end -end -pyjl_handle_error_type(::typeof(pyjlreal_round), self, exc::MethodError) = exc.f === round ? pybuiltins.TypeError : PyNULL - -function init_number() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class NumberValue(AnyValue): - __slots__ = () - def __bool__(self): - return not self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(iszero)))) - def __add__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(+))), other) - def __sub__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(-))), other) - def __mul__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(*))), other) - def __truediv__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(/))), other) - def __floordiv__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(÷))), other) - def __mod__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(%))), other) - def __pow__(self, other, modulo=None): - if modulo is None: - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(^))), other) - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(powermod))), other, modulo) - def __lshift__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(<<))), other) - def __rshift__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(>>))), other) - def __and__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(&))), other) - def __xor__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(⊻))), other) - def __or__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(|))), other) - def __radd__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(+))), other) - def __rsub__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(-))), other) - def __rmul__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(*))), other) - def __rtruediv__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(/))), other) - def __rfloordiv__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(÷))), other) - def __rmod__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(%))), other) - def __rpow__(self, other, modulo=None): - if modulo is None: - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(^))), other) - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(powermod))), other, modulo) - def __rlshift__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(<<))), other) - def __rrshift__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(>>))), other) - def __rand__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(&))), other) - def __rxor__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(⊻))), other) - def __ror__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_rev_op(|))), other) - def __eq__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(==))), other) - def __ne__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(!=))), other) - def __le__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(≤))), other) - def __lt__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(<))), other) - def __ge__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(≥))), other) - def __gt__(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(>))), other) - class ComplexValue(NumberValue): - __slots__ = () - def __complex__(self): - return self._jl_callmethod($(pyjl_methodnum(pycomplex))) - @property - def real(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(real)))) - @property - def imag(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(imag)))) - def conjugate(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(conj)))) - class RealValue(ComplexValue): - __slots__ = () - def __float__(self): - return self._jl_callmethod($(pyjl_methodnum(pyfloat))) - @property - def real(self): - return self - @property - def imag(self): - return 0 - def conjugate(self): - return self - def __complex__(self): - return complex(float(self)) - def __trunc__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlreal_trunc))) - def __floor__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlreal_floor))) - def __ceil__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlreal_ceil))) - def __round__(self, ndigits=None): - return self._jl_callmethod($(pyjl_methodnum(pyjlreal_round)), ndigits) - class RationalValue(RealValue): - __slots__ = () - @property - def numerator(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(numerator)))) - @property - def denominator(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(denominator)))) - class IntegerValue(RationalValue): - __slots__ = () - def __int__(self): - return self._jl_callmethod($(pyjl_methodnum(pyint))) - def __index__(self): - return self.__int__() - @property - def numerator(self): - return self - @property - def denominator(self): - return 1 - import numbers - numbers.Number.register(NumberValue) - numbers.Complex.register(ComplexValue) - numbers.Real.register(RealValue) - numbers.Rational.register(RationalValue) - numbers.Integral.register(IntegerValue) - del numbers - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlnumbertype, jl.NumberValue) - pycopy!(pyjlcomplextype, jl.ComplexValue) - pycopy!(pyjlrealtype, jl.RealValue) - pycopy!(pyjlrationaltype, jl.RationalValue) - pycopy!(pyjlintegertype, jl.IntegerValue) -end - -pyjltype(::Number) = pyjlnumbertype -pyjltype(::Complex) = pyjlcomplextype -pyjltype(::Real) = pyjlrealtype -pyjltype(::Rational) = pyjlrationaltype -pyjltype(::Integer) = pyjlintegertype diff --git a/src/JlWrap/raw.jl b/src/JlWrap/raw.jl deleted file mode 100644 index a531df37..00000000 --- a/src/JlWrap/raw.jl +++ /dev/null @@ -1,143 +0,0 @@ -const pyjlrawtype = pynew() - -pyjlraw_repr(self) = Py("") - -pyjlraw_str(self) = Py(sprint(print, self)) - -pyjl_attr_py2jl(k::String) = replace(k, r"_[b]+$" => (x -> "!"^(length(x) - 1))) - -pyjl_attr_jl2py(k::String) = replace(k, r"!+$" => (x -> "_" * "b"^length(x))) - -function pyjlraw_getattr(self, k_::Py) - k = Symbol(pyjl_attr_py2jl(pyconvert(String, k_))) - pydel!(k_) - pyjlraw(getproperty(self, k)) -end - -function pyjlraw_setattr(self, k_::Py, v_::Py) - k = Symbol(pyjl_attr_py2jl(pyconvert(String, k_))) - pydel!(k_) - v = pyconvert(Any, v_) - setproperty!(self, k, v) - Py(nothing) -end - -pyjlraw_dir(self) = pylist(pyjl_attr_jl2py(string(k)) for k in propertynames(self, true)) - -function pyjlraw_call(self, args_::Py, kwargs_::Py) - if pylen(kwargs_) > 0 - args = pyconvert(Vector{Any}, args_) - kwargs = pyconvert(Dict{Symbol,Any}, kwargs_) - ans = pyjlraw(self(args...; kwargs...)) - elseif pylen(args_) > 0 - args = pyconvert(Vector{Any}, args_) - ans = pyjlraw(self(args...)) - else - ans = pyjlraw(self()) - end - pydel!(args_) - pydel!(kwargs_) - ans -end - -pyjlraw_len(self) = Py(length(self)) - -function pyjlraw_getitem(self, k_::Py) - if pyistuple(k_) - k = pyconvert(Vector{Any}, k_) - pydel!(k_) - pyjlraw(self[k...]) - else - k = pyconvert(Any, k_) - pyjlraw(self[k]) - end -end - -function pyjlraw_setitem(self, k_::Py, v_::Py) - v = pyconvert(Any, v_) - if pyistuple(k_) - k = pyconvert(Vector{Any}, k_) - pydel!(k_) - self[k...] = v - else - k = pyconvert(Any, k_) - self[k] = v - end - Py(nothing) -end - -function pyjlraw_delitem(self, k_::Py) - if pyistuple(k_) - k = pyconvert(Vector{Any}, k_) - pydel!(k_) - delete!(self, k...) - else - k = pyconvert(Any, k_) - delete!(self, k) - end - Py(nothing) -end - -pyjlraw_bool(self::Bool) = Py(self) -pyjlraw_bool(self) = (errset(pybuiltins.TypeError, "Only Julia 'Bool' can be tested for truthyness"); PyNULL) - -function init_raw() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class RawValue(ValueBase): - __slots__ = () - def __repr__(self): - if self._jl_isnull(): - return "" - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_repr))) - def __str__(self): - if self._jl_isnull(): - return "NULL" - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_str))) - def __getattr__(self, k): - if k.startswith("__") and k.endswith("__"): - raise AttributeError(k) - else: - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_getattr)), k) - def __setattr__(self, k, v): - try: - ValueBase.__setattr__(self, k, v) - except AttributeError: - if k.startswith("__") and k.endswith("__"): - raise - else: - return - self._jl_callmethod($(pyjl_methodnum(pyjlraw_setattr)), k, v) - def __dir__(self): - return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlraw_dir))) - def __call__(self, *args, **kwargs): - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_call)), args, kwargs) - def __len__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_len))) - def __getitem__(self, k): - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_getitem)), k) - def __setitem__(self, k, v): - self._jl_callmethod($(pyjl_methodnum(pyjlraw_setitem)), k, v) - def __delitem__(self, k): - self._jl_callmethod($(pyjl_methodnum(pyjlraw_delitem)), k) - def __bool__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_bool))) - def _jl_any(self): - return self._jl_callmethod($(pyjl_methodnum(pyjl))) - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlrawtype, jl.RawValue) -end - -""" - pyjlraw(v) - -Create a Python object wrapping the Julia object `x`. - -It has type `juliacall.RawValue`. This has a much more rigid "Julian" interface than `pyjl(v)`. -For example, accessing attributes or calling this object will always return a `RawValue`. -""" -pyjlraw(v) = pyjl(pyjlrawtype, v) -export pyjlraw diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl index 38c9b82d..47caa9c7 100644 --- a/src/JlWrap/set.jl +++ b/src/JlWrap/set.jl @@ -8,8 +8,6 @@ function pyjlset_discard(x::AbstractSet, v_::Py) Py(nothing) end -pyjlset_clear(x::AbstractSet) = (empty!(x); Py(nothing)) - function pyjlset_pop(x::AbstractSet) if isempty(x) errset(pybuiltins.KeyError, "pop from an empty set") @@ -77,18 +75,14 @@ function init_set() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class SetValue(AnyValue): + class JlSet(JlCollection): __slots__ = () - def __bool__(self): - return bool(len(self)) + def __init__(self, value=None): + JlBase.__init__(self, value, Base.AbstractSet) def add(self, value): return self._jl_callmethod($(pyjl_methodnum(pyjlset_add)), value) def discard(self, value): return self._jl_callmethod($(pyjl_methodnum(pyjlset_discard)), value) - def clear(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_clear))) - def copy(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy))) def pop(self): return self._jl_callmethod($(pyjl_methodnum(pyjlset_pop))) def remove(self, value): @@ -116,10 +110,18 @@ function init_set() def update(self, other): return self._jl_callmethod($(pyjl_methodnum(pyjlset_update)), other) import collections.abc - collections.abc.MutableSet.register(SetValue) + collections.abc.MutableSet.register(JlSet) del collections """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlsettype, jl.SetValue) + pycopy!(pyjlsettype, jl.JlSet) end -pyjltype(::AbstractSet) = pyjlsettype +""" + pyjlset(x::AbstractSet) + +Wrap `x` as a Python `set`-like object. +""" +pyjlset(x::AbstractSet) = pyjl(pyjlsettype, x) +export pyjlset + +Py(x::AbstractSet) = pyjlset(x) diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl deleted file mode 100644 index ef079f5c..00000000 --- a/src/JlWrap/type.jl +++ /dev/null @@ -1,30 +0,0 @@ -const pyjltypetype = pynew() - -function pyjltype_getitem(self::Type, k_) - if pyistuple(k_) - k = pyconvert(Vector{Any}, k_) - pydel!(k_) - Py(self{k...}) - else - k = pyconvert(Any, k_) - Py(self{k}) - end -end - -function init_type() - jl = pyjuliacallmodule - pybuiltins.exec(pybuiltins.compile(""" - $("\n"^(@__LINE__()-1)) - class TypeValue(AnyValue): - __slots__ = () - def __getitem__(self, k): - return self._jl_callmethod($(pyjl_methodnum(pyjltype_getitem)), k) - def __setitem__(self, k, v): - raise TypeError("not supported") - def __delitem__(self, k): - raise TypeError("not supported") - """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjltypetype, jl.TypeValue) -end - -pyjltype(::Type) = pyjltypetype diff --git a/src/JlWrap/vector.jl b/src/JlWrap/vector.jl index 9f728625..7b5074a7 100644 --- a/src/JlWrap/vector.jl +++ b/src/JlWrap/vector.jl @@ -25,11 +25,6 @@ function pyjlvector_reverse(x::AbstractVector) Py(nothing) end -function pyjlvector_clear(x::AbstractVector) - empty!(x) - Py(nothing) -end - function pyjlvector_reversed(x::AbstractVector) Py(reverse(x)) end @@ -39,12 +34,12 @@ function pyjlvector_insert(x::AbstractVector, k_::Py, v_::Py) pydel!(k_) a = axes(x, 1) k′ = k < 0 ? (last(a) + 1 + k) : (first(a) + k) - if checkbounds(Bool, x, k′) || k′ == last(a)+1 + if checkbounds(Bool, x, k′) || k′ == last(a) + 1 v = pyconvertarg(eltype(x), v_, "value") insert!(x, k′, v) return Py(nothing) else - errset(pybuiltins.IndexError, "array index out of bounds"); + errset(pybuiltins.IndexError, "array index out of bounds") return PyNULL end end @@ -121,16 +116,18 @@ function init_vector() jl = pyjuliacallmodule pybuiltins.exec(pybuiltins.compile(""" $("\n"^(@__LINE__()-1)) - class VectorValue(ArrayValue): + class JlVector(JlArray): __slots__ = () + def __init__(self, value=None): + if value is None: + value = Base.Vector() + JlBase.__init__(self, value, Base.AbstractVector) def resize(self, size): return self._jl_callmethod($(pyjl_methodnum(pyjlvector_resize)), size) def sort(self, reverse=False, key=None): return self._jl_callmethod($(pyjl_methodnum(pyjlvector_sort)), reverse, key) def reverse(self): return self._jl_callmethod($(pyjl_methodnum(pyjlvector_reverse))) - def clear(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_clear))) def __reversed__(self): return self._jl_callmethod($(pyjl_methodnum(pyjlvector_reversed))) def insert(self, index, value): @@ -148,10 +145,10 @@ function init_vector() def count(self, value): return self._jl_callmethod($(pyjl_methodnum(pyjlvector_count)), value) import collections.abc - collections.abc.MutableSequence.register(VectorValue) + collections.abc.MutableSequence.register(JlVector) del collections """, @__FILE__(), "exec"), jl.__dict__) - pycopy!(pyjlvectortype, jl.VectorValue) + pycopy!(pyjlvectortype, jl.JlVector) end -pyjltype(::AbstractVector) = pyjlvectortype +pyjlarray(x::AbstractVector) = pyjl(pyjlvectortype, x) diff --git a/src/PythonCall.jl b/src/PythonCall.jl index fdd1774f..f904b63b 100644 --- a/src/PythonCall.jl +++ b/src/PythonCall.jl @@ -38,7 +38,7 @@ for k in [:event_loop_on, :event_loop_off, :fix_qt_plugin_path] end # not API but used in tests -for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlmoduletype, :pyjlintegertype, :pyjlrationaltype, :pyjlrealtype, :pyjlcomplextype, :pyjlsettype, :pyjltypetype] +for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlsettype] @eval using .JlWrap: $k end diff --git a/test/Compat.jl b/test/Compat.jl index 77deb310..a930a564 100644 --- a/test/Compat.jl +++ b/test/Compat.jl @@ -45,7 +45,7 @@ end @testitem "Serialization.jl" begin using Serialization @testset "Py" begin - for x in Py[Py(123), Py(1.23), Py("hello"), pylist([1, 2, 3]), pytuple([1, 2, 3]), Py(nothing), Py([1, 2, 3]), Py(:hello)] + @testset for x in Py[Py(123), Py(1.23), Py("hello"), pylist([1, 2, 3]), pytuple([1, 2, 3]), Py(nothing), Py([1, 2, 3]), Py(:hello)] io = IOBuffer() serialize(io, x) seekstart(io) diff --git a/test/JlWrap.jl b/test/JlWrap.jl index b6364ccd..70e00e28 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -20,20 +20,23 @@ Base.powermod(x::Foo, y::Foo, z::Foo) = "powermod($(x.value), $(y.value), $(z.value))" (x::Foo)(args...; kw...) = "$(x.value)($args)$(length(kw))" Base.getindex(x::Foo, idx...) = "$(x.value)[$idx]" - Base.setindex!(x::Foo, v, idx...) = (x.value = v+sum(idx); x) + Base.setindex!(x::Foo, v, idx...) = (x.value = v + sum(idx); x) Base.delete!(x::Foo, idx...) = (x.value = -sum(idx); x) Base.in(v::Int, x::Foo) = x.value == v Base.nameof(x::Foo) = "nameof $(x.value)" + Base.iterate(x::Foo, st::Int=1) = st <= x.value ? (st, st + 1) : nothing @testset "type" begin @test pyis(pytype(pyjl(Foo(1))), PythonCall.pyjlanytype) @test pyis(pytype(pyjl(nothing)), PythonCall.pyjlanytype) @test pyis(pytype(pyjl(missing)), PythonCall.pyjlanytype) end @testset "bool" begin - @test pytruth(pyjl(Foo(0))) - @test pytruth(pyjl(Foo(1))) - @test pytruth(pyjl(nothing)) - @test pytruth(pyjl(missing)) + @test pytruth(pyjl(true)) + @test !pytruth(pyjl(false)) + @test_throws Exception pytruth(pyjl(Foo(0))) + @test_throws Exception pytruth(pyjl(Foo(1))) + @test_throws Exception pytruth(pyjl(nothing)) + @test_throws Exception pytruth(pyjl(missing)) end @testset "repr" begin @test pyrepr(String, pyjl(missing)) == "Julia: missing" @@ -52,6 +55,9 @@ end @testset "dir" begin @test pycontains(pydir(pyjl(Foo(99))), "value") + @test pycontains(pydir(pyjl(Base)), "+") + @test pycontains(pydir(pyjl(Base)), "Type") + @test pycontains(pydir(pyjl(Base)), "nfields") end @testset "call" begin z = pyjl(Foo(1))(4, 5) @@ -59,6 +65,10 @@ z = pyjl(Foo(1))(4, 5; foo=true, bar=true) @test pyconvert(String, z) == "1((4, 5))2" end + @testset "callback" begin + z = pyjl(Foo(1)).jl_callback(4, 5) + @test pyconvert(String, z) == "1((, ))0" + end @testset "getitem" begin z = pygetitem(pyjl(Foo(1)), 3) @test pyconvert(String, z) == "1[(3,)]" @@ -105,7 +115,7 @@ @test pyconvert(String, z) == "1 + 2" end @testset "radd" begin - z = pyjlraw(Foo(1)) + pyjl(Foo(2)) + z = pyjl(Foo(2)).__radd__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 + 2" end @testset "sub" begin @@ -113,7 +123,7 @@ @test pyconvert(String, z) == "1 - 2" end @testset "rsub" begin - z = pyjlraw(Foo(1)) - pyjl(Foo(2)) + z = pyjl(Foo(2)).__rsub__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 - 2" end @testset "mul" begin @@ -121,7 +131,7 @@ @test pyconvert(String, z) == "1 * 2" end @testset "rmul" begin - z = pyjlraw(Foo(1)) * pyjl(Foo(2)) + z = pyjl(Foo(2)).__rmul__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 * 2" end @testset "truediv" begin @@ -129,7 +139,7 @@ @test pyconvert(String, z) == "1 / 2" end @testset "rtruediv" begin - z = pyjlraw(Foo(1)) / pyjl(Foo(2)) + z = pyjl(Foo(2)).__rtruediv__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 / 2" end @testset "floordiv" begin @@ -137,7 +147,7 @@ @test pyconvert(String, z) == "1 ÷ 2" end @testset "rfloordiv" begin - z = pyjlraw(Foo(1)) ÷ pyjl(Foo(2)) + z = pyjl(Foo(2)).__rfloordiv__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 ÷ 2" end @testset "mod" begin @@ -145,15 +155,15 @@ @test pyconvert(String, z) == "1 % 2" end @testset "rmod" begin - z = pyjlraw(Foo(1)) % pyjl(Foo(2)) + z = pyjl(Foo(2)).__rmod__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 % 2" end @testset "pow" begin - z = pyjl(Foo(1)) ^ pyjl(Foo(2)) + z = pyjl(Foo(1))^pyjl(Foo(2)) @test pyconvert(String, z) == "1 ^ 2" end @testset "rpow" begin - z = pyjlraw(Foo(1)) ^ pyjl(Foo(2)) + z = pyjl(Foo(2)).__rpow__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 ^ 2" end @testset "lshift" begin @@ -161,7 +171,7 @@ @test pyconvert(String, z) == "1 << 2" end @testset "rlshift" begin - z = pyjlraw(Foo(1)) << pyjl(Foo(2)) + z = pyjl(Foo(2)).__rlshift__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 << 2" end @testset "rshift" begin @@ -169,7 +179,7 @@ @test pyconvert(String, z) == "1 >> 2" end @testset "rrshift" begin - z = pyjlraw(Foo(1)) >> pyjl(Foo(2)) + z = pyjl(Foo(2)).__rrshift__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 >> 2" end @testset "and" begin @@ -177,7 +187,7 @@ @test pyconvert(String, z) == "1 & 2" end @testset "rand" begin - z = pyjlraw(Foo(1)) & pyjl(Foo(2)) + z = pyjl(Foo(2)).__rand__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 & 2" end @testset "or" begin @@ -185,7 +195,7 @@ @test pyconvert(String, z) == "1 | 2" end @testset "ror" begin - z = pyjlraw(Foo(1)) | pyjl(Foo(2)) + z = pyjl(Foo(2)).__ror__(pyjl(Foo(1))) @test pyconvert(String, z) == "1 | 2" end @testset "pow3" begin @@ -206,58 +216,265 @@ @test pycontains(z, "text/plain") end @testset "display" begin - pyjl(Foo(1))._jl_display() - pyjl(Foo(1))._jl_display(mime="text/plain") + pyjl(Foo(1)).jl_display() + pyjl(Foo(1)).jl_display(mime="text/plain") end @testset "help" begin - pyjl(Foo(1))._jl_help() - pyjl(Foo(1))._jl_help(mime="text/plain") + pyjl(Foo(1)).jl_help() + pyjl(Foo(1)).jl_help(mime="text/plain") + end + @testset "eval" begin + m = pyjl(Main) + # Basic behavior + z = m.jl_eval("1 + 1") + @test pyisjl(z) + @test pyconvert(Any, z) === 2 + # Trailing whitespace + z = m.jl_eval("1 + 2\n ") + @test pyisjl(z) + @test pyconvert(Any, z) === 3 + end + @testset "to_py $x" for x in [1, 2.3, nothing, "foo"] + y = Py(x) + z = pyjl(x).jl_to_py() + @test pyeq(Bool, pytype(z), pytype(y)) + @test pyeq(Bool, z, y) + end + @testset "iter" begin + z = pylist(pyjl(Foo(3))) + @test all(pyisjl, z) + @test pyeq(Bool, z, pylist([pyjl(1), pyjl(2), pyjl(3)])) + end + @testset "next" begin + z = pynext(pyjl(Foo(3))) + @test pyisjl(z) + @test pyeq(Bool, z, pyjl(1)) + end + @testset "reversed" begin + x = pyjl([1, 2, 3]) + y = x.__reversed__() + @test pyisjl(y) + @test pyjlvalue(y) == [3, 2, 1] + @test pyjlvalue(x) == [1, 2, 3] + end + @testset "int" begin + x = pyjl(34.0) + y = x.__int__() + @test pyisinstance(y, pybuiltins.int) + @test pyeq(Bool, y, 34) + end + @testset "float" begin + x = pyjl(12) + y = x.__float__() + @test pyisinstance(y, pybuiltins.float) + @test pyeq(Bool, y, 12.0) + end + @testset "complex" begin + x = pyjl(Complex(1, 2)) + y = x.__complex__() + @test pyisinstance(y, pybuiltins.complex) + @test pyeq(Bool, y, pycomplex(1, 2)) + end + @testset "index" begin + y = pyjl(12).__index__() + @test pyisinstance(y, pybuiltins.int) + @test pyeq(Bool, y, 12) + @test_throws PyException pyjl(12.0).__index__() + end + @testset "trunc $x" for (x, y) in [(1.1, 1), (8.9, 8), (-1.3, -1), (-7.8, -7)] + z = pyjl(x).__trunc__() + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, y) + end + @testset "floor $x" for (x, y) in [(1.1, 1), (8.9, 8), (-1.3, -2), (-7.8, -8)] + z = pyjl(x).__floor__() + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, y) + end + @testset "ceil $x" for (x, y) in [(1.1, 2), (8.9, 9), (-1.3, -1), (-7.8, -7)] + z = pyjl(x).__ceil__() + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, y) + end + @testset "round $x" for (x, y) in [(1.1, 1), (8.9, 9), (-1.3, -1), (-7.8, -8)] + z = pyjl(x).__round__() + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, y) + end +end + +@testitem "collection" begin + cases = [ + ( + x=fill(nothing), + type=:array, + list=pylist([nothing]), + ), + ( + x=[1, 2, 3], + type=:vector, + list=pylist([1, 2, 3]), + ), + ( + x=[1 2; 3 4], + type=:array, + list=pylist([1, 3, 2, 4]), + ), + ( + x=Set([1, 2, 3]), + type=:set, + list=pylist(Set([1, 2, 3])), + ), + ( + x=Dict(1 => 2, 3 => 4), + type=:dict, + list=pylist(keys(Dict(1 => 2, 3 => 4))), + ), + ( + x=keys(Dict()), + type=:set, + list=pylist(), + ), + ( + x=values(Dict()), + type=:collection, + list=pylist(), + ), + ( + x=(1, 2, 3), + type=:collection, + list=pylist([1, 2, 3]), + ), + ( + x=(x=1, y=2), + type=:collection, + list=pylist([1, 2]), + ), + ( + x=Ref(nothing), + type=:collection, + list=pylist([nothing]), + ),] + @testset "type $(c.x)" for c in cases + y = pyjlcollection(c.x) + @test pyisinstance(y, PythonCall.JlWrap.pyjlcollectiontype) + @test pyis(pytype(y), getproperty(PythonCall.JlWrap, Symbol(:pyjl, c.type, :type))) + end + @testset "len $(c.x)" for c in cases + @test pylen(pyjlcollection(c.x)) == length(c.x) + end + @testset "bool $(c.x)" for c in cases + @test pytruth(pyjlcollection(c.x)) == !isempty(c.x) + end + @testset "iter $(c.x)" for c in cases + @test pyeq(Bool, pylist(pyjlcollection(c.x)), c.list) + end + @testset "hash $(c.x)" for c in cases + # not sure why but the bottom byte doesn't always match + @test mod(pyhash(pyjlcollection(c.x)), UInt32) >> 8 == mod(hash(c.x), UInt32) >> 8 + end + @testset "eq $(c1.x) $(c2.x)" for (c1, c2) in Iterators.product(cases, cases) + @test pyeq(Bool, pyjlcollection(c1.x), pyjlcollection(c2.x)) == (c1.x == c2.x) + end + @testset "contains $(c.x) $(v)" for (c, v) in Iterators.product(cases, [nothing, 0, 1, 2, 3, 4, 5, 0.0, 0.5, 1.0]) + if !isa(c.x, Dict) + @test pycontains(pyjlcollection(c.x), v) == (v in c.x) + end + end + @testset "copy $(c.x)" for c in cases + copyable = try + copy(c.x) + true + catch + false + end + y = pyjlcollection(c.x) + if copyable + z = y.copy() + @test pyis(pytype(y), pytype(z)) + yv = pyjlvalue(y) + zv = pyjlvalue(z) + @test yv === c.x + @test yv == zv + @test yv !== zv + else + @test_throws PyException y.copy() + end + end + @testset "clear $(c.x)" for c in cases + # make a copy or skip the test + x2 = try + copy(c.x) + catch + continue + end + len = length(x2) + # see if the collection can be emptied + clearable = try + empty!(copy(c.x)) + true + catch + false + end + # try clearing the collection + y = pyjlcollection(x2) + @test pylen(y) == len + if clearable + y.clear() + @test pylen(y) == 0 + @test length(x2) == 0 + else + @test_throws PyException y.clear() + @test pylen(y) == len + @test length(x2) == len + end end end @testitem "array" begin @testset "type" begin - @test pyis(pytype(pyjl(fill(nothing))), PythonCall.pyjlarraytype) - @test pyis(pytype(pyjl([1 2; 3 4])), PythonCall.pyjlarraytype) + @test pyis(pytype(pyjlarray(fill(nothing))), PythonCall.pyjlarraytype) + @test pyis(pytype(pyjlarray([1, 2, 3])), PythonCall.pyjlvectortype) + @test pyis(pytype(pyjlarray([1 2; 3 4])), PythonCall.pyjlarraytype) end @testset "bool" begin - @test !pytruth(pyjl(fill(nothing, 0, 1))) - @test !pytruth(pyjl(fill(nothing, 1, 0))) - @test pytruth(pyjl(fill(nothing))) - @test pytruth(pyjl(fill(nothing, 1, 2))) - @test pytruth(pyjl(fill(nothing, 1, 2, 3))) + @test !pytruth(pyjlarray(fill(nothing, 0, 1))) + @test !pytruth(pyjlarray(fill(nothing, 1, 0))) + @test pytruth(pyjlarray(fill(nothing))) + @test pytruth(pyjlarray(fill(nothing, 1, 2))) + @test pytruth(pyjlarray(fill(nothing, 1, 2, 3))) end @testset "ndim" begin - @test pyeq(Bool, pyjl(fill(nothing)).ndim, 0) - @test pyeq(Bool, pyjl(fill(nothing, 1)).ndim, 1) - @test pyeq(Bool, pyjl(fill(nothing, 1, 1)).ndim, 2) - @test pyeq(Bool, pyjl(fill(nothing, 1, 1, 1)).ndim, 3) + @test pyeq(Bool, pyjlarray(fill(nothing)).ndim, 0) + @test pyeq(Bool, pyjlarray(fill(nothing, 1)).ndim, 1) + @test pyeq(Bool, pyjlarray(fill(nothing, 1, 1)).ndim, 2) + @test pyeq(Bool, pyjlarray(fill(nothing, 1, 1, 1)).ndim, 3) end @testset "shape" begin - @test pyeq(Bool, pyjl(fill(nothing)).shape, ()) - @test pyeq(Bool, pyjl(fill(nothing, 3)).shape, (3,)) - @test pyeq(Bool, pyjl(fill(nothing, 3, 5)).shape, (3, 5)) - @test pyeq(Bool, pyjl(fill(nothing, 3, 5, 2)).shape, (3, 5, 2)) + @test pyeq(Bool, pyjlarray(fill(nothing)).shape, ()) + @test pyeq(Bool, pyjlarray(fill(nothing, 3)).shape, (3,)) + @test pyeq(Bool, pyjlarray(fill(nothing, 3, 5)).shape, (3, 5)) + @test pyeq(Bool, pyjlarray(fill(nothing, 3, 5, 2)).shape, (3, 5, 2)) end @testset "getitem" begin - x = pyjl([1, 2, 3, 4, 5]) + x = pyjlarray([1, 2, 3, 4, 5]) @test pyeq(Bool, x[0], 1) @test pyeq(Bool, x[1], 2) @test pyeq(Bool, x[2], 3) @test pyeq(Bool, x[-1], 5) @test pyeq(Bool, x[-2], 4) @test pyeq(Bool, x[-3], 3) - @test pyjlvalue(x[pyslice(3)]) == [1,2,3] - @test pyjlvalue(x[pyslice(2)]) == [1,2] - @test pyjlvalue(x[pyslice(1,2)]) == [2] - @test pyjlvalue(x[pyslice(2,2)]) == [] - @test pyjlvalue(x[pyslice(0,-1)]) == [1,2,3,4] - @test pyjlvalue(x[pyslice(-2,nothing)]) == [4,5] - @test pyjlvalue(x[pyslice(0, 3, 1)]) == [1,2,3] - @test pyjlvalue(x[pyslice(nothing,nothing,2)]) == [1,3,5] - @test pyjlvalue(x[pyslice(1,nothing,2)]) == [2,4] - @test pyjlvalue(x[pyslice(0,nothing,3)]) == [1,4] - x = pyjl([1 2; 3 4]) + @test pyjlvalue(x[pyslice(3)]) == [1, 2, 3] + @test pyjlvalue(x[pyslice(2)]) == [1, 2] + @test pyjlvalue(x[pyslice(1, 2)]) == [2] + @test pyjlvalue(x[pyslice(2, 2)]) == [] + @test pyjlvalue(x[pyslice(0, -1)]) == [1, 2, 3, 4] + @test pyjlvalue(x[pyslice(-2, nothing)]) == [4, 5] + @test pyjlvalue(x[pyslice(0, 3, 1)]) == [1, 2, 3] + @test pyjlvalue(x[pyslice(nothing, nothing, 2)]) == [1, 3, 5] + @test pyjlvalue(x[pyslice(1, nothing, 2)]) == [2, 4] + @test pyjlvalue(x[pyslice(0, nothing, 3)]) == [1, 4] + x = pyjlarray([1 2; 3 4]) @test pyeq(Bool, x[0, 0], 1) @test pyeq(Bool, x[0, 1], 2) @test pyeq(Bool, x[1, 0], 3) @@ -267,54 +484,54 @@ end end @testset "setitem" begin x = [0 0; 0 0] - y = pyjl(x) - y[0,0] = 1 + y = pyjlarray(x) + y[0, 0] = 1 @test x == [1 0; 0 0] - y[0,1] = 2 + y[0, 1] = 2 @test x == [1 2; 0 0] - y[1,0] = 3 + y[1, 0] = 3 @test x == [1 2; 3 0] - y[-1,0] = 4 + y[-1, 0] = 4 @test x == [1 2; 4 0] - y[-2,pyslice(nothing)] = 5 + y[-2, pyslice(nothing)] = 5 @test x == [5 5; 4 0] - y[pyslice(nothing),-1] = 6 + y[pyslice(nothing), -1] = 6 @test x == [5 6; 4 6] - y[pyslice(nothing),pyslice(nothing)] = 7 + y[pyslice(nothing), pyslice(nothing)] = 7 @test x == [7 7; 7 7] end @testset "delitem" begin x = [1, 2, 3, 4, 5, 6, 7, 8] - y = pyjl(x) + y = pyjlarray(x) pydelitem(y, 0) @test x == [2, 3, 4, 5, 6, 7, 8] pydelitem(y, 2) @test x == [2, 3, 5, 6, 7, 8] pydelitem(y, -3) @test x == [2, 3, 5, 7, 8] - pydelitem(y, pyslice(1,nothing,2)) + pydelitem(y, pyslice(1, nothing, 2)) @test x == [2, 5, 8] end @testset "reshape" begin - x = pyjl([1, 2, 3, 4, 5, 6, 7, 8]) + x = pyjlarray([1, 2, 3, 4, 5, 6, 7, 8]) @test pyeq(Bool, x.shape, (8,)) y = x.reshape((2, 4)) @test pyeq(Bool, y.shape, (2, 4)) @test pyjlvalue(y) == [1 3 5 7; 2 4 6 8] end @testset "copy" begin - x = pyjl([1 2; 3 4]) + x = pyjlarray([1 2; 3 4]) y = x.copy() @test pyis(pytype(y), PythonCall.pyjlarraytype) @test pyjlvalue(x) == pyjlvalue(y) @test typeof(pyjlvalue(x)) == typeof(pyjlvalue(y)) @test pyjlvalue(x) !== pyjlvalue(y) - x[0,0] = 0 + x[0, 0] = 0 @test pyjlvalue(x) == [0 2; 3 4] @test pyjlvalue(y) == [1 2; 3 4] end @testset "array_interface" begin - x = pyjl(Float32[1 2 3; 4 5 6]).__array_interface__ + x = pyjlarray(Float32[1 2 3; 4 5 6]).__array_interface__ @test pyisinstance(x, pybuiltins.dict) @test pyeq(Bool, x["shape"], (2, 3)) @test pyeq(Bool, x["typestr"], "1, "two"=>2))) + @test !pytruth(pyjldict(Dict())) + @test pytruth(pyjldict(Dict("one" => 1, "two" => 2))) end -end - -@testitem "io" begin - @testset "type" begin - @test pyis(pytype(pyjl(devnull)), PythonCall.pyjlbinaryiotype) - @test pyis(pytype(pybinaryio(devnull)), PythonCall.pyjlbinaryiotype) - @test pyis(pytype(pytextio(devnull)), PythonCall.pyjltextiotype) + @testset "iter" begin + @test pyeq(Bool, pyset(pyjldict(Dict())), pyset()) + @test pyeq(Bool, pyset(pyjldict(Dict(1 => 2, 3 => 4))), pyset([1, 3])) end - @testset "bool" begin - @test pytruth(pybinaryio(devnull)) - @test pytruth(pytextio(devnull)) + @testset "contains" begin + x = pyjldict(Dict(1 => 2, 3 => 4)) + @test pycontains(x, 1) + @test !pycontains(x, 2) + @test pycontains(x, 3) + @test !pycontains(x, 4) + @test !pycontains(x, 1.2) + @test pycontains(x, 1.0) + @test !pycontains(x, nothing) + end + @testset "getitem" begin + x = pyjldict(Dict(1 => 2, 3 => 4)) + @test pyisinstance(x[1], pybuiltins.int) + @test pyeq(Bool, x[1], 2) + @test pyisinstance(x[3], pybuiltins.int) + @test pyeq(Bool, x[3], 4) + @test_throws PyException x[2] + end + @testset "setitem" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + y[1] = pyint(11) + @test x[1] === 11 + y[2] = pyfloat(22.0) + @test x[2] === 22 + end + @testset "delitem" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + pydelitem(y, 3) + @test x == Dict(1 => 2) + pydelitem(y, 1) + @test x == Dict() + end + @testset "keys" begin + x = pyjldict(Dict(1 => 2, 3 => 4)).keys() + @test all(pyisinstance(k, pybuiltins.int) for k in x) + @test pyeq(Bool, pyset(x), pyset([1, 3])) + end + @testset "values" begin + x = pyjldict(Dict(1 => 2, 3 => 4)).values() + @test all(pyisinstance(k, pybuiltins.int) for k in x) + @test pyeq(Bool, pyset(x), pyset([2, 4])) + end + @testset "items" begin + x = pyjldict(Dict(1 => 2, 3 => 4)).items() + @test all(pyisinstance(i, pybuiltins.tuple) for i in x) + @test all(pylen(i) == 2 for i in x) + @test all(pyisinstance(i[0], pybuiltins.int) for i in x) + @test all(pyisinstance(i[1], pybuiltins.int) for i in x) + @test pyeq(Bool, pyset(x), pyset([(1, 2), (3, 4)])) + end + @testset "get" begin + x = pyjldict(Dict(1 => 2, 3 => 4)) + y = x.get(1) + @test pyisinstance(y, pybuiltins.int) + @test pyeq(Bool, y, 2) + y = x.get(3) + @test pyisinstance(y, pybuiltins.int) + @test pyeq(Bool, y, 4) + y = x.get(5) + @test pyis(y, pybuiltins.None) + y = x.get(5, 0) + @test pyisinstance(y, pybuiltins.int) + @test pyeq(Bool, y, 0) + end + @testset "setdefault" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + z = y.setdefault(1, 0) + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, 2) + @test x == Dict(1 => 2, 3 => 4) + z = y.setdefault(2, 0) + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, 0) + @test x == Dict(1 => 2, 3 => 4, 2 => 0) + z = y.setdefault(2, 99) + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, 0) + @test x == Dict(1 => 2, 3 => 4, 2 => 0) + end + @testset "pop" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + z1 = y.pop(1) + @test pyisinstance(z1, pybuiltins.int) + @test pyeq(Bool, z1, 2) + @test x == Dict(3 => 4) + @test_throws PyException y.pop(2) + z2 = y.pop(3) + @test pyisinstance(z2, pybuiltins.int) + @test pyeq(Bool, z2, 4) + @test x == Dict() + end + @testset "popitem" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + z1 = y.popitem() + z2 = y.popitem() + @test all(pyisinstance(z, pybuiltins.tuple) for z in [z1, z2]) + @test all(pylen(z) == 2 for z in [z1, z2]) + @test all(pyisinstance(z[0], pybuiltins.int) for z in [z1, z2]) + @test all(pyisinstance(z[1], pybuiltins.int) for z in [z1, z2]) + @test pyeq(Bool, pyset([z1, z2]), pyset([(1, 2), (3, 4)])) + end + @testset "update" begin + x = Dict(1 => 2, 3 => 4) + y = pyjldict(x) + y.update(pydict([(3, 3.0), (2, 2.0)])) + @test x == Dict(1 => 2, 2 => 2, 3 => 3) end end -@testitem "iter" begin - x1 = [1,2,3,4,5] - x2 = pyjl(x1) - x3 = pylist(x2) - x4 = pyconvert(Vector{Int}, x3) - @test x1 == x4 +@testitem "io/base" begin + @testset "close" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + @test isopen(x) + y.close() + @test !isopen(x) + end + @testset "closed" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + z = y.closed + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, false) + close(x) + z = y.closed + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, true) + end + @testset "fileno" begin + # IOBuffer has no fileno + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + @test_throws PyException y.fileno() + # check some file that has a fileno + mktemp() do name, x + y = pytextio(x) + z = y.fileno() + @test pyisinstance(z, pybuiltins.int) + @test pyeq(Bool, z, fd(x)) + end + end + @testset "flush" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + y.flush() + # TODO: check it actually flushed something + end + @testset "isatty $(typeof(x))" for (x, y) in [(IOBuffer(), false), (devnull, false), (stdout, false)] + # TODO: how to get a TTY in a test environment?? + z = pytextio(x).isatty() + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, y) + end + @testset "readable $(typeof(x))" for (x, y) in [(IOBuffer(), true), (devnull, false), (stdin, true)] + z = pytextio(x).readable() + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, y) + end + @testset "readlines" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pytextio(x) + z = y.readlines() + @test pyisinstance(z, pybuiltins.list) + @test pylen(z) == 2 + @test all(pyisinstance(line, pybuiltins.str) for line in z) + @test pyeq(Bool, z, pylist(["hello\n", "world\n"])) + end + @testset "seek" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + @test position(x) == 0 + zs = Py[] + push!(zs, y.seek(1)) + @test position(x) == 1 + push!(zs, y.seek(2, 0)) + @test position(x) == 2 + push!(zs, y.seek(1, 1)) + @test position(x) == 3 + push!(zs, y.seek(-2, 2)) + @test position(x) == 4 + @test all(pyisinstance(z, pybuiltins.int) for z in zs) + @test pyeq(Bool, pylist(zs), pylist([1, 2, 3, 4])) + end + @testset "seekable $(typeof(x))" for (x, y) in [(IOBuffer(), true), (devnull, true), (stdin, true), (stdout, true)] + z = pytextio(x).seekable() + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, y) + end + @testset "tell" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + zs = Py[] + @test position(x) == 0 + push!(zs, y.tell()) + seek(x, 5) + push!(zs, y.tell()) + @test all(pyisinstance(z, pybuiltins.int) for z in zs) + @test pyeq(Bool, pylist(zs), pylist([0, 5])) + end + @testset "truncate" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + y.truncate(5) + seekend(x) + @test position(x) == 5 + seek(x, 3) + y.truncate() + seekstart(x) + seekend(x) + @test position(x) == 3 + end + @testset "writable $(typeof(x))" for (x, y) in [(IOBuffer(), true), (IOBuffer(""), false), (devnull, true), (stdout, true)] + z = pytextio(x).writable() + @test pyisinstance(z, pybuiltins.bool) + @test pyeq(Bool, z, y) + end + @testset "writelines" begin + x = IOBuffer() + y = pytextio(x) + y.writelines(pylist(["test\n", "message\n"])) + seekstart(x) + @test readline(x) == "test" + @test readline(x) == "message" + @test readline(x) == "" + end + @testset "enter/exit" begin + x = IOBuffer() + seekstart(x) + y = pytextio(x) + @test isopen(x) + r = pywith(y) do z + @test pyis(z, y) + 12 + end + @test r === 12 + @test !isopen(x) + # same again by cause an error + x = IOBuffer() + seekstart(x) + y = pytextio(x) + @test isopen(x) + @test_throws PyException pywith(y) do z + z.invalid_attr + end + @test !isopen(x) # should still get closed + end + @testset "iter" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pytextio(x) + zs = pylist(y) + @test all(pyisinstance(z, pybuiltins.str) for z in zs) + @test pyeq(Bool, zs, pylist(["hello\n", "world\n"])) + end + @testset "next" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pytextio(x) + zs = Py[] + push!(zs, y.__next__()) + push!(zs, y.__next__()) + @test_throws PyException y.__next__() + @test all(pyisinstance(z, pybuiltins.str) for z in zs) + @test pyeq(Bool, pylist(zs), pylist(["hello\n", "world\n"])) + end end -@testitem "module" begin +@testitem "io/binary" begin @testset "type" begin - @test pyis(pytype(pyjl(PythonCall)), PythonCall.pyjlmoduletype) + @test pyis(pytype(pybinaryio(devnull)), PythonCall.pyjlbinaryiotype) end @testset "bool" begin - @test pytruth(pyjl(PythonCall)) + @test pytruth(pybinaryio(devnull)) end - @testset "seval" begin - m = Py(Main) - @test pyconvert(Any, m.seval("1 + 1")) === 2 # Basic behavior - @test pyconvert(Any, m.seval("1 + 1\n ")) === 2 # Trailing whitespace + @testset "detach" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pybinaryio(x) + @test_throws PyException y.detach() + end + @testset "read" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pybinaryio(x) + z = y.read() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"hello\nworld\n")) + z = y.read() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"")) + end + @testset "read1" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pybinaryio(x) + z = y.read1() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"hello\nworld\n")) + z = y.read1() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"")) + end + @testset "readline" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pybinaryio(x) + z = y.readline() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"hello\n")) + z = y.readline() + @test pyisinstance(z, pybuiltins.bytes) + @test pyeq(Bool, z, pybytes(b"")) + end + @testset "readinto" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pybinaryio(x) + z = pybuiltins.bytearray(pybytes(b"xxxxxxxxxx")) + n = y.readinto(z) + @test pyisinstance(n, pybuiltins.int) + @test pyeq(Bool, n, 6) + @test pyeq(Bool, pybytes(z), pybytes(b"hello\nxxxx")) + end + @testset "readinto1" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pybinaryio(x) + z = pybuiltins.bytearray(pybytes(b"xxxxxxxxxx")) + n = y.readinto(z) + @test pyisinstance(n, pybuiltins.int) + @test pyeq(Bool, n, 6) + @test pyeq(Bool, pybytes(z), pybytes(b"hello\nxxxx")) + end + @testset "write" begin + x = IOBuffer() + print(x, "hello\n") + y = pybinaryio(x) + y.write(pybytes(b"world\n")) + @test String(take!(x)) == "hello\nworld\n" end end -@testitem "number" begin +@testitem "io/text" begin @testset "type" begin - @test pyis(pytype(pyjl(false)), PythonCall.pyjlintegertype) - @test pyis(pytype(pyjl(0)), PythonCall.pyjlintegertype) - @test pyis(pytype(pyjl(0//1)), PythonCall.pyjlrationaltype) - @test pyis(pytype(pyjl(0.0)), PythonCall.pyjlrealtype) - @test pyis(pytype(pyjl(Complex(0.0))), PythonCall.pyjlcomplextype) + @test pyis(pytype(pytextio(devnull)), PythonCall.pyjltextiotype) end @testset "bool" begin - @test !pytruth(pyjl(false)) - @test !pytruth(pyjl(0)) - @test !pytruth(pyjl(0//1)) - @test !pytruth(pyjl(0.0)) - @test !pytruth(pyjl(Complex(0.0))) - @test pytruth(pyjl(true)) - @test pytruth(pyjl(3)) - @test pytruth(pyjl(5//2)) - @test pytruth(pyjl(2.3)) - @test pytruth(pyjl(Complex(1.2, 3.4))) + @test pytruth(pytextio(devnull)) + end + @testset "encoding" begin + x = IOBuffer() + print(x, "hello\n") + y = pytextio(x) + z = y.encoding + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "UTF-8") + end + @testset "errors" begin + x = IOBuffer() + print(x, "hello\n") + y = pytextio(x) + z = y.errors + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "strict") + end + @testset "detach" begin + x = IOBuffer() + print(x, "hello\n") + y = pytextio(x) + @test_throws PyException y.detach() + end + @testset "read" begin + x = IOBuffer() + print(x, "hello\n") + print(x, "world\n") + seekstart(x) + y = pytextio(x) + z = y.read() + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "hello\nworld\n") + z = y.read() + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "") + end + @testset "readline" begin + x = IOBuffer() + print(x, "hello\n") + seekstart(x) + y = pytextio(x) + z = y.readline() + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "hello\n") + z = y.readline() + @test pyisinstance(z, pybuiltins.str) + @test pyeq(Bool, z, "") + end + @testset "write" begin + x = IOBuffer() + print(x, "hello\n") + y = pytextio(x) + y.write("world!") + @test String(take!(x)) == "hello\nworld!" end end -@testitem "objectarray" begin - +@testitem "iter" begin + x1 = [1, 2, 3, 4, 5] + x2 = pyjl(x1) + x3 = pylist(x2) + x4 = pyconvert(Vector{Int}, x3) + @test x1 == x4 end -@testitem "raw" begin +@testitem "objectarray" begin end @testitem "set" begin @testset "type" begin - @test pyis(pytype(pyjl(Set())), PythonCall.pyjlsettype) + @test pyis(pytype(pyjlset(Set())), PythonCall.pyjlsettype) end @testset "bool" begin - @test !pytruth(pyjl(Set())) - @test pytruth(pyjl(Set([1,2,3]))) + @test !pytruth(pyjlset(Set())) + @test pytruth(pyjlset(Set([1, 2, 3]))) end -end - -@testitem "type" begin - @testset "type" begin - @test pyis(pytype(pyjl(Int)), PythonCall.pyjltypetype) + @testset "add" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.add(1) + @test x == Set([1, 2, 3]) + y.add(0) + @test x == Set([0, 1, 2, 3]) + @test_throws PyException y.add(nothing) + end + @testset "discard" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.discard(1) + @test x == Set([2, 3]) + y.discard(1) + @test x == Set([2, 3]) end - @testset "bool" begin - @test pytruth(pyjl(Int)) + @testset "pop" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + zs = Py[] + for i in 1:3 + push!(zs, y.pop()) + @test length(x) == 3 - i + end + @test_throws PyException y.pop() + @test all(pyisinstance(z, pybuiltins.int) for z in zs) + @test pyeq(Bool, pyset(zs), pyset([1, 2, 3])) + end + @testset "remove" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.remove(1) + @test x == Set([2, 3]) + @test_throws PyException y.remove(1) + end + @testset "difference" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.difference(pyset([1, 3, 5])) + @test pyeq(Bool, z, pyset([2])) + end + @testset "intersection" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.intersection(pyset([1, 3, 5])) + @test pyeq(Bool, z, pyset([1, 3])) + end + @testset "symmetric_difference" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.symmetric_difference(pyset([1, 3, 5])) + @test pyeq(Bool, z, pyset([2, 5])) + end + @testset "union" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.union(pyset([1, 3, 5])) + @test pyeq(Bool, z, pyset([1, 2, 3, 5])) + end + @testset "isdisjoint" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.isdisjoint(pyset([1, 3, 5])) + @test pyeq(Bool, z, false) + z = y.isdisjoint(pyset([0, 5])) + @test pyeq(Bool, z, true) + end + @testset "issubset" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.issubset(pyset([1, 3, 5])) + @test pyeq(Bool, z, false) + z = y.issubset(pyset([1, 2, 3, 4, 5])) + @test pyeq(Bool, z, true) + end + @testset "issuperset" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + z = y.issuperset(pyset([1, 3, 5])) + @test pyeq(Bool, z, false) + z = y.issuperset(pyset([1, 3])) + @test pyeq(Bool, z, true) + end + @testset "difference_update" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.difference_update(pyset([1, 3, 5])) + @test x == Set([2]) + end + @testset "intersection_update" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.intersection_update(pyset([1, 3, 5])) + @test x == Set([1, 3]) + end + @testset "symmetric_difference_update" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.symmetric_difference_update(pyset([1, 3, 5])) + @test x == Set([2, 5]) + end + @testset "update" begin + x = Set([1, 2, 3]) + y = pyjlset(x) + y.update(pyset([1, 3, 5])) + @test x == Set([1, 2, 3, 5]) end end @testitem "vector" begin @testset "type" begin - @test pyis(pytype(pyjl([1, 2, 3, 4])), PythonCall.pyjlvectortype) + @test pyis(pytype(pyjlarray([1, 2, 3, 4])), PythonCall.pyjlvectortype) end @testset "bool" begin - @test !pytruth(pyjl([])) - @test pytruth(pyjl([1])) - @test pytruth(pyjl([1,2])) + @test !pytruth(pyjlarray([])) + @test pytruth(pyjlarray([1])) + @test pytruth(pyjlarray([1, 2])) end @testset "resize" begin - x = pyjl([1, 2, 3, 4, 5]) + x = pyjlarray([1, 2, 3, 4, 5]) @test pyjlvalue(x) == [1, 2, 3, 4, 5] x.resize(5) @test pyjlvalue(x) == [1, 2, 3, 4, 5] @@ -469,91 +1197,91 @@ end @test pyjlvalue(x) == [5, 6] end @testset "sort" begin - x = pyjl([4,6,2,3,7,6,1]) + x = pyjlarray([4, 6, 2, 3, 7, 6, 1]) x.sort() - @test pyjlvalue(x) == [1,2,3,4,6,6,7] - x = pyjl([4,6,2,3,7,6,1]) + @test pyjlvalue(x) == [1, 2, 3, 4, 6, 6, 7] + x = pyjlarray([4, 6, 2, 3, 7, 6, 1]) x.sort(reverse=true) - @test pyjlvalue(x) == [7,6,6,4,3,2,1] - x = pyjl([4,-6,2,-3,7,-6,1]) + @test pyjlvalue(x) == [7, 6, 6, 4, 3, 2, 1] + x = pyjlarray([4, -6, 2, -3, 7, -6, 1]) x.sort(key=abs) - @test pyjlvalue(x) == [1,2,-3,4,-6,-6,7] - x = pyjl([4,-6,2,-3,7,-6,1]) + @test pyjlvalue(x) == [1, 2, -3, 4, -6, -6, 7] + x = pyjlarray([4, -6, 2, -3, 7, -6, 1]) x.sort(key=abs, reverse=true) - @test pyjlvalue(x) == [7,-6,-6,4,-3,2,1] + @test pyjlvalue(x) == [7, -6, -6, 4, -3, 2, 1] end @testset "reverse" begin - x = pyjl([1,2,3,4,5]) + x = pyjlarray([1, 2, 3, 4, 5]) x.reverse() - @test pyjlvalue(x) == [5,4,3,2,1] + @test pyjlvalue(x) == [5, 4, 3, 2, 1] end @testset "clear" begin - x = pyjl([1,2,3,4,5]) - @test pyjlvalue(x) == [1,2,3,4,5] + x = pyjlarray([1, 2, 3, 4, 5]) + @test pyjlvalue(x) == [1, 2, 3, 4, 5] x.clear() @test pyjlvalue(x) == [] end @testset "reversed" begin - x = pyjl([1,2,3,4,5]) + x = pyjlarray([1, 2, 3, 4, 5]) y = pybuiltins.reversed(x) - @test pyjlvalue(x) == [1,2,3,4,5] - @test pyjlvalue(y) == [5,4,3,2,1] + @test pyjlvalue(x) == [1, 2, 3, 4, 5] + @test pyjlvalue(y) == [5, 4, 3, 2, 1] end @testset "insert" begin - x = pyjl([1,2,3]) + x = pyjlarray([1, 2, 3]) x.insert(0, 4) - @test pyjlvalue(x) == [4,1,2,3] + @test pyjlvalue(x) == [4, 1, 2, 3] x.insert(2, 5) - @test pyjlvalue(x) == [4,1,5,2,3] + @test pyjlvalue(x) == [4, 1, 5, 2, 3] x.insert(5, 6) - @test pyjlvalue(x) == [4,1,5,2,3,6] + @test pyjlvalue(x) == [4, 1, 5, 2, 3, 6] x.insert(-3, 7) - @test pyjlvalue(x) == [4,1,5,7,2,3,6] + @test pyjlvalue(x) == [4, 1, 5, 7, 2, 3, 6] @test_throws PyException x.insert(10, 10) end @testset "append" begin - x = pyjl([1,2,3]) + x = pyjlarray([1, 2, 3]) x.append(4) - @test pyjlvalue(x) == [1,2,3,4] + @test pyjlvalue(x) == [1, 2, 3, 4] x.append(5.0) - @test pyjlvalue(x) == [1,2,3,4,5] + @test pyjlvalue(x) == [1, 2, 3, 4, 5] @test_throws PyException x.append(nothing) @test_throws PyException x.append(1.2) @test_throws PyException x.append("2") end @testset "extend" begin - x = pyjl([1,2,3]) + x = pyjlarray([1, 2, 3]) x.extend(pylist()) - @test pyjlvalue(x) == [1,2,3] - x.extend(pylist([4,5])) - @test pyjlvalue(x) == [1,2,3,4,5] + @test pyjlvalue(x) == [1, 2, 3] + x.extend(pylist([4, 5])) + @test pyjlvalue(x) == [1, 2, 3, 4, 5] x.extend(pylist([6.0])) - @test pyjlvalue(x) == [1,2,3,4,5,6] + @test pyjlvalue(x) == [1, 2, 3, 4, 5, 6] end @testset "pop" begin - x = pyjl([1,2,3,4,5]) + x = pyjlarray([1, 2, 3, 4, 5]) @test pyeq(Bool, x.pop(), 5) - @test pyjlvalue(x) == [1,2,3,4] + @test pyjlvalue(x) == [1, 2, 3, 4] @test pyeq(Bool, x.pop(0), 1) - @test pyjlvalue(x) == [2,3,4] + @test pyjlvalue(x) == [2, 3, 4] @test pyeq(Bool, x.pop(1), 3) - @test pyjlvalue(x) == [2,4] + @test pyjlvalue(x) == [2, 4] @test pyeq(Bool, x.pop(-2), 2) @test pyjlvalue(x) == [4] @test_throws PyException x.pop(10) end @testset "remove" begin - x = pyjl([1,3,2,4,5,3,1]) - @test pyjlvalue(x) == [1,3,2,4,5,3,1] + x = pyjlarray([1, 3, 2, 4, 5, 3, 1]) + @test pyjlvalue(x) == [1, 3, 2, 4, 5, 3, 1] x.remove(3) - @test pyjlvalue(x) == [1,2,4,5,3,1] + @test pyjlvalue(x) == [1, 2, 4, 5, 3, 1] @test_throws PyException x.remove(0) @test_throws PyException x.remove(nothing) @test_throws PyException x.remove("2") - @test pyjlvalue(x) == [1,2,4,5,3,1] + @test pyjlvalue(x) == [1, 2, 4, 5, 3, 1] end @testset "index" begin - x = pyjl([1,3,2,4,5,2,1]) + x = pyjlarray([1, 3, 2, 4, 5, 2, 1]) @test pyeq(Bool, x.index(1), 0) @test pyeq(Bool, x.index(2), 2) @test pyeq(Bool, x.index(3), 1) @@ -566,7 +1294,7 @@ end @test_throws PyException x.index("2") end @testset "count" begin - x = pyjl([1,2,3,4,5,1,2,3,1]) + x = pyjlarray([1, 2, 3, 4, 5, 1, 2, 3, 1]) @test pyeq(Bool, x.count(0), 0) @test pyeq(Bool, x.count(1), 3) @test pyeq(Bool, x.count(2), 2) diff --git a/test/PyMacro.jl b/test/PyMacro.jl index d3a1b1b0..cb8fc848 100644 --- a/test/PyMacro.jl +++ b/test/PyMacro.jl @@ -84,11 +84,11 @@ @test x isa Py @test pyis(pytype(x), pybuiltins.dict) @test pyeq(Bool, x, pydict()) - x = @py {x=1, y=2} + x = @py {x = 1, y = 2} @test x isa Py @test pyis(pytype(x), pybuiltins.dict) @test pyeq(Bool, x, pydict(x=1, y=2)) - x = @py {"x": 1, "y": 2} + x = @py {"x":1, "y":2} @test x isa Py @test pyis(pytype(x), pybuiltins.dict) @test pyeq(Bool, x, pydict(x=1, y=2)) @@ -197,35 +197,59 @@ @test pyis(_ver, sys.version_info) end @testset "short-circuit" begin - x = @py 3 && pylist([1,2]) + x = @py 3 && @jl(pylist([1, 2])) @test pyeq(Bool, x, pylist([1, 2])) x = @py None && True @test pyis(x, pybuiltins.None) - x = @py None || 0 || pyset() + x = @py None || 0 || @jl(pyset()) @test pyeq(Bool, x, pyset()) - x = @py pydict() || 8 || "" + x = @py @jl(pydict()) || 8 || "" @test pyeq(Bool, x, 8) end @testset "if" begin - x = @py if 1 == 2; "a"; end + x = @py if 1 == 2 + "a" + end @test x isa Py @test pyis(x, pybuiltins.None) - x = @py if 1 < 2; "a"; end + x = @py if 1 < 2 + "a" + end @test x isa Py @test pyeq(Bool, x, "a") - x = @py if 1 == 2; "a"; else; "b"; end + x = @py if 1 == 2 + "a" + else + "b" + end @test x isa Py @test pyeq(Bool, x, "b") - x = @py if 1 < 2; "a"; else; "b"; end + x = @py if 1 < 2 + "a" + else + "b" + end @test x isa Py @test pyeq(Bool, x, "a") - x = @py if 1 == 2; "a"; elseif 1 < 2; "b"; end + x = @py if 1 == 2 + "a" + elseif 1 < 2 + "b" + end @test x isa Py @test pyeq(Bool, x, "b") - x = @py if 1 < 2; "a"; elseif 2 < 3; "b"; end + x = @py if 1 < 2 + "a" + elseif 2 < 3 + "b" + end @test x isa Py @test pyeq(Bool, x, "a") - x = @py if 1 == 2; "a"; elseif 2 == 3; "b"; end + x = @py if 1 == 2 + "a" + elseif 2 == 3 + "b" + end @test x isa Py @test pyis(x, pybuiltins.None) end