diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 38e901fd..a1143c79 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 373a9c29..dbf9ca09 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - v1 push: branches: - main diff --git a/Project.toml b/Project.toml index 81186cc7..cbeaa6ec 100644 --- a/Project.toml +++ b/Project.toml @@ -14,6 +14,14 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39" +[weakdeps] +CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" + +[extensions] +CategoricalArraysExt = "CategoricalArrays" +PyCallExt = "PyCall" + [compat] CategoricalArrays = "0.10, 1" CondaPkg = "0.2.33" @@ -28,17 +36,9 @@ Tables = "1" UnsafePointers = "1" julia = "1.10" -[extensions] -PyCallExt = "PyCall" -CategoricalArraysExt = "CategoricalArrays" - [extras] CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" -[weakdeps] -CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" -PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" - [workspace] projects = ["test", "docs", "benchmark"] diff --git a/docs/make.jl b/docs/make.jl index 7b04641e..9966e378 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,6 +20,7 @@ makedocs( "compat.md", "faq.md", "releasenotes.md", + "v1-migration-guide.md", ], ) diff --git a/docs/src/conversion-to-julia.md b/docs/src/conversion-to-julia.md index 67d1e904..5c65a2aa 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 7ced4080..bcbc2e1d 100644 --- a/docs/src/conversion-to-python.md +++ b/docs/src/conversion-to-python.md @@ -26,16 +26,13 @@ From Python, this occurs when converting the return value of a Julia function. | `Dates.Time` | `datetime.time` | | `Dates.DateTime` | `datetime.datetime` | | `Dates.Second`, `Dates.Millisecond`, `Dates.Microsecond`, `Dates.Nanosecond` | `datetime.timedelta` | -| `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) @@ -47,11 +44,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 a31dff7b..fd9f2260 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -30,7 +30,7 @@ Related issues: ## 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 0aa8e6d7..73a25a4f 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,75 +33,71 @@ 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(mime=None)`: Display the object using Julia's display mechanism. -- `_jl_help(mime=None)`: Display help for the object. -- `_jl_call_nogil(*args, **kwargs)`: Call this with the GIL disabled. +- `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_call_nogil(*args, **kwargs)`: Call this with the GIL disabled. +- `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. @@ -131,22 +115,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. @@ -157,66 +139,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.MutableMapping` 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.MutableSet` 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).) -- `_jl_call_nogil(*args, **kwargs)`: Call this with the GIL disabled. -````` diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md index 26182add..affea99e 100644 --- a/docs/src/juliacall.md +++ b/docs/src/juliacall.md @@ -55,11 +55,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 @@ -69,7 +69,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/pythoncall-reference.md b/docs/src/pythoncall-reference.md index f09d5f12..27b0d1b8 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 dcbf7f75..bde8d317 100644 --- a/docs/src/releasenotes.md +++ b/docs/src/releasenotes.md @@ -1,5 +1,29 @@ # Release Notes +## Unreleased (v1) +* The vast majority of these changes are breaking, see the [v1 Migration Guide](@ref) for how to upgrade. +* Changes to core functionality: + * Comparisons like `==(::Py, ::Py)`, `<(::Py, ::Number)`, `isless(::Number, ::Py)` now return `Bool` instead of `Py`. +* Changes to `PythonCall.GC` (now more like `Base.GC`): + * `enable(true)` replaces `enable()`. + * `enable(false)` replaces `disable()`. + * `gc()` added. +* Changes to Python wrapper types: + * `PyArray` has been reparametrised from `PyArray{T,N,M,L,R}` to `PyArray{T,N,F}`: + * `F` is a tuple of symbols representing flags, with `:linear` replacing `L` and `:mutable` replacing `M`. + * `R` is removed and is now implied by `T`, which currently must be either a bits type (equal to `R`) or `Py`, or a tuple of these. +* 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 * Minimum supported Python version is now 3.10. * Minimum supported Julia version is now 1.10. diff --git a/docs/src/v1-migration-guide.md b/docs/src/v1-migration-guide.md new file mode 100644 index 00000000..87950289 --- /dev/null +++ b/docs/src/v1-migration-guide.md @@ -0,0 +1,101 @@ +# v1 Migration Guide + +Use this guide to help with migrating code from v0.9 to v1. + +## Core functionality + +Comparisons (`==`, `<`, etc.) between Python objects `Py`, or between `Py` and `Number`, +used to return `Py` but now return `Bool`. The old behaviour was a pun but broke the +Base API behaviour of these functions. These comparisons will now raise an error if the +underlying Python operation does not return `bool`. + +* Instead of `pytruth(Py(3) < Py(5))` use `Py(3) < Py(5)`. +* Instead of `Py(3) < Py(5)` use `Py(Py(3) < Py(5))`. +* Instead of `np.array([1,2,3]) < Py(3)` use `pylt(np.array([1,2,3]), Py(3))`. This is + because comparisons on numpy arrays return arrays of `bool` rather than a single + `bool`. +* Instead of `pylt(Bool, Py(3), Py(5))` you can use `Py(3) < Py(5)`. + +## `PythonCall.GC` + +This submodule has been changed to closer mimic the `Base.GC` API. + +* Instead of `PythonCall.GC.enable()` use `PythonCall.GC.enable(true)`. +* Instead of `PythonCall.GC.disable()` use `PythonCall.GC.enable(false)`. + +## Python wrappers (`PyArray`, etc.) + +`PyArray` has been reparametrised from `PyArray{T,N,L,M,R}` to `PyArray{T,N,F}` where +`F` is a `Tuple` of `Symbol` flags replacing `L` (now `:linear`) and `M` +(now `:mutable`). The `R` parameter (the underlying raw type) is removed and now implied +by `T`. + +* Instead of `PyArray{Int,2,true,true,Int}` use `PyArray{Int,2,(:mutable,:linear)}`. +* Instead of `PyArray{Bool,1,false,false,Bool}` use `PyArray{Bool,1,()}`. +* Instead of `PyArray{Py,2,false,false,PythonCall.Wrap.UnsafePyObject}` use `PyArray{Py,2,()}`. + +Because the `R` parameter is removed, if the underlying array is of Python objects, the +`PyArray` must have eltype `Py`. Previously you could construct a `PyArray{String}` from +such a thing and the elements would be automatically `pyconvert(String, element)`-ed for +you. + +* Instead of `PyArray{String}(x)` use `pyconvert.(String, PyArray{Py}(x))` if you are + OK with taking a copy. Or use `mappedarray(x->pyconvert(String, x), PyArray{Py}(x))` + from [MappedArrays.jl](https://github.com/JuliaArrays/MappedArrays.jl) to emulate the + old behaviour. +* Same comments for `pyconvert(PyArray{String}, x)`. + +## Julia wrappers (`JlDict`, etc.) + +The wrapper types have been renamed. + +* Instead of `juliacall.AnyValue` use `juliacall.Jl` (but see below). +* Instead of `juliacall.ArrayValue` use `juliacall.JlArray`. +* Instead of `juliacall.DictValue` use `juliacall.JlDict`. + +Most methods on the `Jl` class return a `Jl` now instead of an arbitrary Python object +converted from the Julia return value. This makes generic programming easier and +more closely reflects the behaviour of `Py`. + +* Instead of `jl.seval("1+2")` use `jl.jl_eval("1+2").jl_to_py()`. +* Instead of `jl.rand(5)[0]` use `jl.rand(5)[1].jl_to_py()`. Note the shift from 0-based + to 1-based indexing - previously `jl.rand(5)` was a `juliacall.VectorValue` which + supported Python 0-based indexing, but now `jl.rand(5)` is a `juliacall.Jl` which + supports indexing by passing the arguments directly to Julia, which is 1-based. + +Some wrapper types have been removed and can mostly be replaced with `Jl`. + +* Instead of `juliacall.RawValue` use `juliacall.Jl`, since this behaves much the same + now. +* Instead of `juliacall.IntegerValue` (and other number types) use `int`, `float`, + `complex` or other numeric types as appropriate. Alternatively use `juliacall.Jl` + which supports the basic arithmetic and comparison operators, but is not strictly a + number. +* Instead of `juliacall.ModuleValue` use `juliacall.Jl`. The only benefit of + `ModuleValue` was its `seval` method, which is now `Jl.jl_eval`. +* Instead of `juliacall.TypeValue` use `juliacall.Jl`. The only benefit of `TypeValue` + was that indexing syntax (`jl.Vector[jl.Type]`) was converted to Julia's curly syntax + (`Vector{Type}`) but `Jl` does this now (for types). + +Methods with the `_jl_` prefix are renamed with the `jl_` prefix: +* Instead of `x._jl_help()` use `x.jl_help()`. +* Instead of `x._jl_display()` use `x.jl_display()`. + +The `seval` function is now called `jl_eval`: +* Instead of `juliacall.Main.seval("1+2")` use `juliacall.Main.jl_eval("1+2")`. + +Other methods, functions and attributes removed: +* Instead of `x._jl_raw()` use `x` (if already a `Jl`) or `Jl(x)`. This is because the + old `AnyValue` and `RawValue` are replaced by `Jl`. +* Instead of `juliacall.convert(type, value)` use `juliacall.Jl(value, type)`. +* Instead of `juliacall.Pkg` you must import import it yourself, such as + `juliacall.Main.jl_eval("using Pkg; Pkg")`. + +On the Julia side, the `pyjl` function now always returns a `Jl`, whereas before it +would return one of the more specific wrappers (now called `JlDict`, `JlArray`, etc.). + +* Instead of `pyjl([1, 2, 3])` use `pyjlarray([1, 2, 3])` if you need a `JlArray`. +* Instead of `pyjl(Dict())` use `pyjldict(Dict())` if you need a `JlDict`. +* Instead of `pyjl(Set())` use `pyjlset(Set())` if you need a `JlSet`. +* Continue to use `pyjl` if you are OK with the result being a `Jl`. +* Note that `Py([1, 2, 3])` still returns a `JlArray`, etc., only `pyjl` itself changed. 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 f0f7cb40..e08e04ff 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 9cdc8ce4..7ed9fbb5 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -10,21 +10,11 @@ def test_newmodule(): 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 @@ -60,15 +50,15 @@ 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(): @@ -76,11 +66,11 @@ def test_issue_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 @@ -94,7 +84,7 @@ def test_issue_433(): def test_julia_gc(): from juliacall import Main as jl - if jl.seval('v"1.11.0-" <= VERSION < v"1.11.3"'): + if jl.jl_eval('v"1.11.0-" <= VERSION < v"1.11.3"'): # Seems to be a Julia bug - hopefully fixed in 1.11.3 pytest.skip("Test not yet supported on Julia 1.11+") @@ -105,7 +95,7 @@ def test_julia_gc(): # Debugging note: if you get segfaults, then run the tests with # `PYTHON_JULIACALL_HANDLE_SIGNALS=yes python3 -X faulthandler -m pytest -p no:faulthandler -s --nbval --cov=pysrc ./pytest/` # in order to recover a bit more information from the segfault. - jl.seval( + jl.jl_eval( """ using PythonCall, Test let @@ -120,10 +110,8 @@ def test_julia_gc(): ) -@pytest.mark.parametrize( - ["yld", "raw"], [(yld, raw) for yld in [False, True] for raw in [False, True]] -) -def test_call_nogil(yld, raw): +@pytest.mark.parametrize("yld", [True, False]) +def test_call_nogil(yld): """Tests that we can execute Julia code in parallel by releasing the GIL.""" from concurrent.futures import ThreadPoolExecutor, wait from time import time @@ -136,10 +124,7 @@ def test_call_nogil(yld, raw): else: # use Libc.systemsleep which does not yield jsleep = jl.Libc.systemsleep - if raw: - # test RawValue instead of AnyValue - jsleep = jsleep._jl_raw() - jsleep = jsleep._jl_call_nogil + jsleep = jsleep.jl_call_nogil jyield = getattr(jl, "yield") # precompile jsleep(0.01) diff --git a/src/API/exports.jl b/src/API/exports.jl index 3d476182..0454e0e1 100644 --- a/src/API/exports.jl +++ b/src/API/exports.jl @@ -127,6 +127,10 @@ export pyclassmethod export pyfunc export pyisjl export pyjl +export pyjlcollection +export pyjlarray +export pyjldict +export pyjlset export pyjlraw export pyjltype export pyjlvalue diff --git a/src/API/functions.jl b/src/API/functions.jl index ab1258c8..0b8689bc 100644 --- a/src/API/functions.jl +++ b/src/API/functions.jl @@ -116,6 +116,10 @@ function pyclassmethod end function pyfunc end function pyisjl end function pyjl end +function pyjlcollection end +function pyjlarray end +function pyjlset end +function pyjldict end function pyjlraw end function pyjltype end function pyjlvalue end diff --git a/src/API/types.jl b/src/API/types.jl index 55a4211e..14acece8 100644 --- a/src/API/types.jl +++ b/src/API/types.jl @@ -51,7 +51,7 @@ baremodule pybuiltins end # Wrap """ - PyArray{T,N,M,L,R}(x; copy=true, array=true, buffer=true) + PyArray{T,N,F}(x; copy=true, array=true, buffer=true) Wrap the Python array `x` as a Julia `AbstractArray{T,N}`. @@ -62,31 +62,34 @@ If `copy=false` then the resulting array is guaranteed to directly wrap the data The type parameters are all optional, and are: - `T`: The element type. - `N`: The number of dimensions. -- `M`: True if the array is mutable. -- `L`: True if the array supports fast linear indexing. -- `R`: The element type of the underlying buffer. Often equal to `T`. +- `F`: Tuple of symbols, including: + - `:mutable`: The array is mutable. + - `:linear`: Supports fast linear indexing. + - `:contiguous`: Data is F-contiguous. Implies `:linear`. """ -struct PyArray{T,N,M,L,R} <: AbstractArray{T,N} - ptr::Ptr{R} # pointer to the data +struct PyArray{T,N,F} <: AbstractArray{T,N} + ptr::Ptr{Cvoid} # pointer to the data length::Int # length of the array size::NTuple{N,Int} # size of the array strides::NTuple{N,Int} # strides (in bytes) between elements py::Py # underlying python object handle::Py # the data in this array is valid as long as this handle is alive - function PyArray{T,N,M,L,R}( + function PyArray{T,N,F}( ::Val{:new}, - ptr::Ptr{R}, + ptr::Ptr, size::NTuple{N,Int}, strides::NTuple{N,Int}, py::Py, handle::Py, - ) where {T,N,M,L,R} + ) where {T,N,F} T isa Type || error("T must be a Type") N isa Int || error("N must be an Int") - M isa Bool || error("M must be a Bool") - L isa Bool || error("L must be a Bool") - R isa DataType || error("R must be a DataType") - new{T,N,M,L,R}(ptr, prod(size), size, strides, py, handle) + F isa Tuple{Vararg{Symbol}} || error("F must be a tuple of Symbols") + for flag in F + flag in (:mutable, :linear, :contiguous, :copy, :nocopy) || + error("invalid entry of F: $(repr(flag))") + end + new{T,N,F}(ptr, prod(size), size, strides, py, handle) end end diff --git a/src/Core/Py.jl b/src/Core/Py.jl index 8b26f036..02a2fee5 100644 --- a/src/Core/Py.jl +++ b/src/Core/Py.jl @@ -363,34 +363,12 @@ Base.broadcastable(x::Py) = Ref(x) (f::Py)(args...; kwargs...) = pycall(f, args...; kwargs...) # comparisons -Base.:(==)(x::Py, y::Py) = pyeq(x, y) -Base.:(!=)(x::Py, y::Py) = pyne(x, y) -Base.:(<=)(x::Py, y::Py) = pyle(x, y) -Base.:(<)(x::Py, y::Py) = pylt(x, y) -Base.:(>=)(x::Py, y::Py) = pyge(x, y) -Base.:(>)(x::Py, y::Py) = pygt(x, y) +Base.:(==)(x::Py, y::Py) = pyeq(Bool, x, y) +Base.:(<=)(x::Py, y::Py) = pyle(Bool, x, y) +Base.:(<)(x::Py, y::Py) = pylt(Bool, x, y) Base.isless(x::Py, y::Py) = pylt(Bool, x, y) Base.isequal(x::Py, y::Py) = pyeq(Bool, x, y) -# we also allow comparison with numbers -Base.:(==)(x::Py, y::Number) = pyeq(x, y) -Base.:(!=)(x::Py, y::Number) = pyne(x, y) -Base.:(<=)(x::Py, y::Number) = pyle(x, y) -Base.:(<)(x::Py, y::Number) = pylt(x, y) -Base.:(>=)(x::Py, y::Number) = pyge(x, y) -Base.:(>)(x::Py, y::Number) = pygt(x, y) -Base.isless(x::Py, y::Number) = pylt(Bool, x, y) -Base.isequal(x::Py, y::Number) = pyeq(Bool, x, y) - -Base.:(==)(x::Number, y::Py) = pyeq(x, y) -Base.:(!=)(x::Number, y::Py) = pyne(x, y) -Base.:(<=)(x::Number, y::Py) = pyle(x, y) -Base.:(<)(x::Number, y::Py) = pylt(x, y) -Base.:(>=)(x::Number, y::Py) = pyge(x, y) -Base.:(>)(x::Number, y::Py) = pygt(x, y) -Base.isless(x::Number, y::Py) = pylt(Bool, x, y) -Base.isequal(x::Number, y::Py) = pyeq(Bool, x, y) - Base.zero(::Type{Py}) = pyint(0) Base.one(::Type{Py}) = pyint(1) @@ -416,44 +394,7 @@ Base.xor(x::Py, y::Py) = pyxor(x, y) Base.:(|)(x::Py, y::Py) = pyor(x, y) Base.:(^)(x::Py, y::Py) = pypow(x, y) -# also allow binary arithmetic with numbers -Base.:(+)(x::Number, y::Py) = pyadd(x, y) -Base.:(-)(x::Number, y::Py) = pysub(x, y) -Base.:(*)(x::Number, y::Py) = pymul(x, y) -# Base.:(+)(x::Number, y::Py) = pymatmul(x, y) -Base.div(x::Number, y::Py) = pyfloordiv(x, y) -Base.:(/)(x::Number, y::Py) = pytruediv(x, y) -Base.rem(x::Number, y::Py) = pymod(x, y) -# Base.:(+)(x::Number, y::Py) = pydivmod(x, y) -Base.:(<<)(x::Number, y::Py) = pylshift(x, y) -Base.:(>>)(x::Number, y::Py) = pyrshift(x, y) -Base.:(&)(x::Number, y::Py) = pyand(x, y) -Base.xor(x::Number, y::Py) = pyxor(x, y) -Base.:(|)(x::Number, y::Py) = pyor(x, y) -Base.:(^)(x::Number, y::Py) = pypow(x, y) - -Base.:(+)(x::Py, y::Number) = pyadd(x, y) -Base.:(-)(x::Py, y::Number) = pysub(x, y) -Base.:(*)(x::Py, y::Number) = pymul(x, y) -# Base.:(+)(x::Py, y::Number) = pymatmul(x, y) -Base.div(x::Py, y::Number) = pyfloordiv(x, y) -Base.:(/)(x::Py, y::Number) = pytruediv(x, y) -Base.rem(x::Py, y::Number) = pymod(x, y) -# Base.:(+)(x::Py, y::Number) = pydivmod(x, y) -Base.:(<<)(x::Py, y::Number) = pylshift(x, y) -Base.:(>>)(x::Py, y::Number) = pyrshift(x, y) -Base.:(&)(x::Py, y::Number) = pyand(x, y) -Base.xor(x::Py, y::Number) = pyxor(x, y) -Base.:(|)(x::Py, y::Number) = pyor(x, y) -Base.:(^)(x::Py, y::Number) = pypow(x, y) - Base.powermod(x::Py, y::Py, z::Py) = pypow(x, y, z) -Base.powermod(x::Number, y::Py, z::Py) = pypow(x, y, z) -Base.powermod(x::Py, y::Number, z::Py) = pypow(x, y, z) -Base.powermod(x::Py, y::Py, z::Number) = pypow(x, y, z) -Base.powermod(x::Number, y::Number, z::Py) = pypow(x, y, z) -Base.powermod(x::Number, y::Py, z::Number) = pypow(x, y, z) -Base.powermod(x::Py, y::Number, z::Number) = pypow(x, y, z) # documentation function Base.Docs.getdoc(x::Py, @nospecialize(sig) = Union{}) diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl index 50c17987..d3ed3d15 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,64 @@ 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 +108,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 @@ -268,8 +323,8 @@ function _pyjl_deserialize(t::C.PyPtr, v::C.PyPtr) end end -const _pyjlbase_name = "juliacall.ValueBase" -const _pyjlbase_isnull_name = "_jl_isnull" +const _pyjlbase_name = "juliacall.JlBase" +const _pyjlbase_type = fill(C.PyTypeObject()) const _pyjlbase_callmethod_name = "_jl_callmethod" const _pyjlbase_reduce_name = "__reduce__" const _pyjlbase_serialize_name = "_jl_serialize" @@ -289,11 +344,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)), @@ -311,7 +361,7 @@ function init_c() ), C.PyMethodDef(), ) - + # Create members for weakref support empty!(_pyjlbase_members) push!( @@ -324,20 +374,36 @@ function init_c() ), C.PyMemberDef(), # NULL terminator ) - + # Create slots for PyType_Spec empty!(_pyjlbase_slots) push!( _pyjlbase_slots, - C.PyType_Slot(slot = C.Py_tp_new, pfunc = @cfunction(_pyjl_new, C.PyPtr, (C.PyPtr, C.PyPtr, C.PyPtr))), - C.PyType_Slot(slot = C.Py_tp_dealloc, pfunc = @cfunction(_pyjl_dealloc, Cvoid, (C.PyPtr,))), + C.PyType_Slot( + slot = C.Py_tp_new, + pfunc = @cfunction(_pyjl_new, C.PyPtr, (C.PyPtr, C.PyPtr, C.PyPtr)) + ), + C.PyType_Slot( + slot = C.Py_tp_dealloc, + pfunc = @cfunction(_pyjl_dealloc, Cvoid, (C.PyPtr,)) + ), + C.PyType_Slot( + slot = C.Py_tp_init, + pfunc = @cfunction(_pyjl_init, Cint, (C.PyPtr, C.PyPtr, C.PyPtr)) + ), C.PyType_Slot(slot = C.Py_tp_methods, pfunc = pointer(_pyjlbase_methods)), C.PyType_Slot(slot = C.Py_tp_members, pfunc = pointer(_pyjlbase_members)), - C.PyType_Slot(slot = C.Py_bf_getbuffer, pfunc = @cfunction(_pyjl_get_buffer, Cint, (C.PyPtr, Ptr{C.Py_buffer}, Cint))), - C.PyType_Slot(slot = C.Py_bf_releasebuffer, pfunc = @cfunction(_pyjl_release_buffer, Cvoid, (C.PyPtr, Ptr{C.Py_buffer}))), + C.PyType_Slot( + slot = C.Py_bf_getbuffer, + pfunc = @cfunction(_pyjl_get_buffer, Cint, (C.PyPtr, Ptr{C.Py_buffer}, Cint)) + ), + C.PyType_Slot( + slot = C.Py_bf_releasebuffer, + pfunc = @cfunction(_pyjl_release_buffer, Cvoid, (C.PyPtr, Ptr{C.Py_buffer})) + ), C.PyType_Slot(), # NULL terminator ) - + # Create PyType_Spec _pyjlbase_spec[] = C.PyType_Spec( name = pointer(_pyjlbase_name), @@ -345,27 +411,67 @@ function init_c() flags = C.Py_TPFLAGS_BASETYPE | C.Py_TPFLAGS_HAVE_VERSION_TAG, slots = pointer(_pyjlbase_slots), ) - + # Create type using PyType_FromSpec o = PyJuliaBase_Type[] = C.PyType_FromSpec(pointer(_pyjlbase_spec)) if o == C.PyNULL 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__() init_c() end -PyJuliaValue_IsNull(o) = Base.GC.@preserve o UnsafePtr{PyJuliaValueObject}(C.asptr(o)).value[] == 0 +PyJuliaValue_Check(o) = + Base.GC.@preserve o C.PyObject_IsInstance(C.asptr(o), PyJuliaBase_Type[]) + +PyJuliaValue_GetValue(o) = Base.GC.@preserve o begin + v = UnsafePtr{PyJuliaValueObject}(C.asptr(o)).value[] + if v == 0 + nothing + elseif v > 0 + PYJLVALUES[v] + elseif v == -1 + false + elseif v == -2 + true + end +end -PyJuliaValue_GetValue(o) = Base.GC.@preserve o PYJLVALUES[UnsafePtr{PyJuliaValueObject}(C.asptr(o)).value[]] +PyJuliaValue_SetValue(o, v::Union{Nothing,Bool}) = Base.GC.@preserve o begin + optr = UnsafePtr{PyJuliaValueObject}(C.asptr(o)) + idx = optr.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 + optr.value[] = idx + nothing +end -PyJuliaValue_SetValue(_o, @nospecialize(v)) = Base.GC.@preserve _o begin - o = C.asptr(_o) - idx = UnsafePtr{PyJuliaValueObject}(o).value[] - if idx == 0 +PyJuliaValue_SetValue(o, @nospecialize(v)) = Base.GC.@preserve o begin + optr = UnsafePtr{PyJuliaValueObject}(C.asptr(o)) + idx = optr.value[] + if idx >= 1 + PYJLVALUES[idx] = v + else if isempty(PYJLFREEVALUES) push!(PYJLVALUES, v) idx = length(PYJLVALUES) @@ -373,23 +479,32 @@ PyJuliaValue_SetValue(_o, @nospecialize(v)) = Base.GC.@preserve _o begin idx = pop!(PYJLFREEVALUES) PYJLVALUES[idx] = v end - UnsafePtr{PyJuliaValueObject}(o).value[] = idx - else - PYJLVALUES[idx] = v + optr.value[] = idx end nothing end -PyJuliaValue_New(_t, @nospecialize(v)) = Base.GC.@preserve _t begin - t = C.asptr(_t) - if C.PyType_IsSubtype(t, PyJuliaBase_Type[]) != 1 +PyJuliaValue_New(t, @nospecialize(v)) = Base.GC.@preserve t begin + tptr = C.asptr(t) + if C.PyType_IsSubtype(tptr, PyJuliaBase_Type[]) != 1 C.PyErr_SetString( C.POINTERS.PyExc_TypeError, - "Expecting a subtype of 'juliacall.ValueBase'", + "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(tptr) + err = C.PyTuple_SetItem(args, 0, tptr) + 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 79b9c6b6..2ace6b85 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 @@ -24,27 +24,26 @@ import ..PythonCall: pyisjl, pyjlvalue, pyjlraw, + pyjlcollection, + pyjlarray, + pyjldict, + pyjlset, pybinaryio, pytextio, PyObjectVector, PyObjectMatrix, PyObjectArray -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") @@ -53,24 +52,18 @@ include("callback.jl") function __init__() 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 diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl index 96a6a3d2..0e044ace 100644 --- a/src/JlWrap/any.jl +++ b/src/JlWrap/any.jl @@ -1,4 +1,5 @@ const pyjlanytype = pynew() +const pyjlitertype = pynew() # pyjlany_repr(self) = Py("") function pyjlany_repr(self) @@ -9,16 +10,20 @@ function pyjlany_repr(self) ) # 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 @@ -36,18 +41,29 @@ 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 = Py(self(args...)) + ans = pyjl(self(args...)) else - ans = Py(self()) + ans = pyjl(self()) end pydel!(args_) pydel!(kwargs_) @@ -56,32 +72,61 @@ 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()) + end + pydel!(args_) + pydel!(kwargs_) + ans +end +pyjl_handle_error_type(::typeof(pyjlany_callback), self, exc::MethodError) = + exc.f === self ? pybuiltins.TypeError : PyNULL + function pyjlany_call_nogil(self, args_::Py, kwargs_::Py) if pylen(kwargs_) > 0 args = pyconvert(Vector{Any}, args_) kwargs = pyconvert(Dict{Symbol,Any}, kwargs_) - ans = Py(GIL.@unlock self(args...; kwargs...)) + ans = pyjl(GIL.@unlock self(args...; kwargs...)) elseif pylen(args_) > 0 args = pyconvert(Vector{Any}, args_) - ans = Py(GIL.@unlock self(args...)) + ans = pyjl(GIL.@unlock self(args...)) else - ans = Py(GIL.@unlock self()) + ans = pyjl(GIL.@unlock self()) end pydel!(args_) pydel!(kwargs_) ans end -pyjl_handle_error_type(::typeof(pyjlany_call_nogil), self, exc) = - exc isa MethodError && exc.f === self ? pybuiltins.TypeError : PyNULL +pyjl_handle_error_type(::typeof(pyjlany_call_nogil), 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) = @@ -119,33 +164,36 @@ 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) -pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = - exc isa MethodError && exc.f === in ? pybuiltins.TypeError : PyNULL +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::MethodError) = + exc.f === in ? pybuiltins.TypeError : PyNULL struct pyjlany_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 @@ -157,28 +205,30 @@ 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))) -pyjl_handle_error_type(::typeof(pyjlany_name), self, exc) = - exc isa MethodError && exc.f === nameof ? pybuiltins.AttributeError : PyNULL +pyjlany_name(self) = Py(string(nameof(self))::String) +pyjl_handle_error_type(::typeof(pyjlany_name), self, exc::MethodError) = + exc.f === nameof ? pybuiltins.AttributeError : PyNULL function pyjlany_display(self, mime_::Py) mime = pyconvertarg(Union{Nothing,String}, mime_, "mime") @@ -195,7 +245,10 @@ function pyjlany_help(self, mime_::Py) mime = pyconvertarg(Union{Nothing,String}, mime_, "mime") doc = Docs.getdoc(self) if doc === nothing - doc = Docs.doc(self) + # hack: the relevant methods of Docs.doc are actually + # in REPL, so we load it dynamically if needed + @eval Main using REPL + doc = invokelatest(Docs.doc, self) end x = Utils.ExtraNewline(doc) if mime === nothing @@ -225,24 +278,144 @@ 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) @@ -250,7 +423,7 @@ class AnyValue(ValueBase): 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 @@ -258,11 +431,11 @@ class AnyValue(ValueBase): 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): @@ -273,6 +446,8 @@ class AnyValue(ValueBase): 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): @@ -283,6 +458,8 @@ class AnyValue(ValueBase): 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): @@ -339,6 +516,8 @@ class AnyValue(ValueBase): 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): @@ -351,21 +530,41 @@ class AnyValue(ValueBase): 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): - '''Convert this to a juliacall.RawValue.''' - return self._jl_callmethod($(pyjl_methodnum(pyjlraw))) - def _jl_display(self, mime=None): + def jl_display(self, mime=None): '''Display this, optionally specifying the MIME type.''' return self._jl_callmethod($(pyjl_methodnum(pyjlany_display)), mime) - def _jl_help(self, mime=None): + def jl_help(self, mime=None): '''Show help for this Julia object.''' return self._jl_callmethod($(pyjl_methodnum(pyjlany_help)), mime) - def _jl_call_nogil(self, *args, **kwargs): + 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 jl_call_nogil(self, *args, **kwargs): '''Call this with the given arguments but with the GIL disabled. WARNING: This function must not interact with Python at all without re-acquiring @@ -380,30 +579,15 @@ class AnyValue(ValueBase): ), 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`. - -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)`. -""" -pyjl(v) = pyjl(pyjltype(v), v) + pyjl(x) +Create a Python `juliacall.Jl` object wrapping the Julia object `x`. """ - pyjltype(x) +pyjl(v) = pyjl(pyjlanytype, v) -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 +pyjliter(x) = pyjl(pyjlitertype, x) diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl index f063c191..a52a62de 100644 --- a/src/JlWrap/array.jl +++ b/src/JlWrap/array.jl @@ -299,8 +299,8 @@ pytypestrdescr(::Type{T}) where {T} = 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)) @@ -330,57 +330,65 @@ function init_array() pybuiltins.exec( pybuiltins.compile( """ -$("\n"^(@__LINE__()-1)) -class ArrayValue(AnyValue): - __slots__ = () - _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info)) - @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): - self._jl_callmethod($(pyjl_methodnum(pyjlarray_setitem)), k, v) - def __delitem__(self, k): - self._jl_callmethod($(pyjl_methodnum(pyjlarray_delitem)), k) - @property - def __array_interface__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlarray_array_interface))) - def __array__(self, dtype=None): - # convert to an array-like object - arr = self - if not (hasattr(arr, "__array_interface__") or hasattr(arr, "__array_struct__")): - # the first attempt collects into an Array - arr = self._jl_callmethod($(pyjl_methodnum(pyjlarray_array__array))) - if not (hasattr(arr, "__array_interface__") or hasattr(arr, "__array_struct__")): - # the second attempt collects into a PyObjectArray - arr = self._jl_callmethod($(pyjl_methodnum(pyjlarray_array__pyobjectarray))) - # convert to a numpy array if numpy is available - try: - import numpy - arr = numpy.array(arr, dtype=dtype) - except ImportError: - pass - return arr - def to_numpy(self, dtype=None, copy=True, order="K"): - import numpy - return numpy.array(self, dtype=dtype, copy=copy, order=order) -""", + $("\n"^(@__LINE__()-1)) + 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 reshape(self, shape): + return self._jl_callmethod($(pyjl_methodnum(pyjlarray_reshape)), shape) + def __getitem__(self, k): + return self._jl_callmethod($(pyjl_methodnum(pyjlarray_getitem)), k) + def __setitem__(self, k, v): + self._jl_callmethod($(pyjl_methodnum(pyjlarray_setitem)), k, v) + def __delitem__(self, k): + self._jl_callmethod($(pyjl_methodnum(pyjlarray_delitem)), k) + @property + def __array_interface__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlarray_array_interface))) + def __array__(self, dtype=None): + # convert to an array-like object + arr = self + if not (hasattr(arr, "__array_interface__") or hasattr(arr, "__array_struct__")): + # the second attempt collects into a PyObjectArray + arr = self._jl_callmethod($(pyjl_methodnum(pyjlarray_array__pyobjectarray))) + # convert to a numpy array if numpy is available + try: + import numpy + except ImportError: + numpy = None + if numpy is not None: + return numpy.array(arr, dtype=dtype, copy=copy, order=order) + return arr + def to_numpy(self, dtype=None, copy=True, order="K"): + 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) + +Py(x::AbstractArray) = pyjlarray(x) diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl index 3a8b6c6b..2497f4b0 100644 --- a/src/JlWrap/base.jl +++ b/src/JlWrap/base.jl @@ -9,38 +9,24 @@ pyjl(t, v) = pynew(errcheck(@autopy t Cjl.PyJuliaValue_New(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) -pyjlisnull(x) = @autopy x begin - if pyisjl(x_) - Cjl.PyJuliaValue_IsNull(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_) 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} = @@ -48,10 +34,6 @@ pyconvert_rule_jlvalue(::Type{T}, x::Py) where {T} = 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 @@ -97,10 +79,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 @@ -116,7 +95,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 8fdd48d1..8ad36a3a 100644 --- a/src/JlWrap/callback.jl +++ b/src/JlWrap/callback.jl @@ -1,75 +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]) @@ -77,7 +8,7 @@ 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. @@ -85,7 +16,7 @@ they are never converted. function pyfunc( f; name = nothing, - qualname = name, + qualname = nothing, doc = nothing, signature = nothing, wrap = pywrapcallback, @@ -96,13 +27,27 @@ function pyfunc( 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 @@ -135,8 +80,8 @@ If `f` is not a Python object (e.g. if `f` is a `Function`) then it is converted pystaticmethod(f; kw...) = pybuiltins.staticmethod(ispy(f) ? f : pyfunc(f; kw...)) """ - 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. @@ -144,11 +89,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...) diff --git a/src/JlWrap/collection.jl b/src/JlWrap/collection.jl new file mode 100644 index 00000000..18facc2f --- /dev/null +++ b/src/JlWrap/collection.jl @@ -0,0 +1,64 @@ +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) + +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 52a61fe3..b41e4a0f 100644 --- a/src/JlWrap/dict.jl +++ b/src/JlWrap/dict.jl @@ -9,13 +9,9 @@ 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) = @@ -40,82 +36,87 @@ function init_dict() pybuiltins.exec( pybuiltins.compile( """ -$("\n"^(@__LINE__()-1)) -class DictValue(AnyValue): - __slots__ = () - _jl_undefined_ = object() - def __bool__(self): - return bool(len(self)) - def __iter__(self): - return self._jl_callmethod($(pyjl_methodnum(pyjldict_iter))) - def __contains__(self, key): - return self._jl_callmethod($(pyjl_methodnum(pyjldict_contains)), key) - def __getitem__(self, key): - if key in self: - return self._jl_callmethod($(pyjl_methodnum(pyjldict_getitem)), key) - else: - raise KeyError(key) - def __setitem__(self, key, value): - return self._jl_callmethod($(pyjl_methodnum(pyjldict_setitem)), key, value) - def __delitem__(self, key): - if key in self: - return self._jl_callmethod($(pyjl_methodnum(pyjldict_delitem)), key) - else: - raise KeyError(key) - def keys(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ keys))) - def values(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ values))) - def items(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ DictPairSet))) - def get(self, key, default=None): - if key in self: - return self[key] - else: - return default - def setdefault(self, key, default=None): - 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] - del self[key] - return ans - elif default is self._jl_undefined_: - raise KeyError(key) - else: - return default - def popitem(self): - if len(self): - return self._jl_callmethod($(pyjl_methodnum(Py ∘ pop!))) - else: - raise KeyError() - def update(self, other=_jl_undefined_, **kwargs): - if other is self._jl_undefined_: - pass - else: - if hasattr(other, "keys"): - items = ((k, other[k]) for k in other.keys()) - else: - items = other - 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) -del collections -""", + $("\n"^(@__LINE__()-1)) + class JlDict(JlCollection): + __slots__ = () + _jl_undefined_ = object() + 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(pyjliter ∘ Iterator ∘ keys))) + def __contains__(self, key): + return self._jl_callmethod($(pyjl_methodnum(pyjldict_contains)), key) + def __getitem__(self, key): + if key in self: + return self._jl_callmethod($(pyjl_methodnum(pyjldict_getitem)), key) + else: + raise KeyError(key) + def __setitem__(self, key, value): + return self._jl_callmethod($(pyjl_methodnum(pyjldict_setitem)), key, value) + def __delitem__(self, key): + if key in self: + return self._jl_callmethod($(pyjl_methodnum(pyjldict_delitem)), key) + else: + raise KeyError(key) + def keys(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ keys))) + def values(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlcollection ∘ values))) + def items(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ DictPairSet))) + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + def setdefault(self, key, default=None): + if key not in self: + self[key] = default + return self[key] + def pop(self, key, default=_jl_undefined_): + if key in self: + ans = self[key] + del self[key] + return ans + elif default is self._jl_undefined_: + raise KeyError(key) + else: + return default + def popitem(self): + if len(self): + return self._jl_callmethod($(pyjl_methodnum(Py ∘ pop!))) + else: + raise KeyError() + def update(self, other=_jl_undefined_, **kwargs): + if other is self._jl_undefined_: + pass + else: + if hasattr(other, "keys"): + items = ((k, other[k]) for k in other.keys()) + else: + items = other + self._jl_callmethod($(pyjl_methodnum(pyjldict_update)), items) + if kwargs: + self.update(kwargs) + import collections.abc + 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) + +Py(x::AbstractDict) = pyjldict(x) diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl index 5fa54a57..3070e3fe 100644 --- a/src/JlWrap/io.jl +++ b/src/JlWrap/io.jl @@ -10,7 +10,7 @@ pyjlio_closed(io::IO) = Py(!isopen(io)) pyjl_handle_error_type(::typeof(pyjlio_closed), io, exc) = exc isa MethodError && exc.f === isopen ? pybuiltins.ValueError : PyNULL -pyjlio_fileno(io::IO) = Py(fd(io)) +pyjlio_fileno(io::IO) = Py(Base.cconvert(Cint, fd(io))::Cint) pyjl_handle_error_type(::typeof(pyjlio_fileno), io, exc) = exc isa MethodError && exc.f === fd ? pybuiltins.ValueError : PyNULL @@ -227,104 +227,108 @@ function init_io() pybuiltins.exec( pybuiltins.compile( """ -$("\n"^(@__LINE__()-1)) -class IOValueBase(AnyValue): - __slots__ = () - def close(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_close))) - @property - def closed(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_closed))) - def fileno(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_fileno))) - def flush(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_flush))) - def isatty(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_isatty))) - def readable(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_readable))) - def readlines(self, hint=-1): - lines = [] - total = 0 - while hint < 0 or total < hint: - line = self.readline() - if line: - lines.append(line) - total += len(line) - else: - break - return lines - def seek(self, offset, whence=0): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_seek)), offset, whence) - def seekable(self): - return True - def tell(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_tell))) - def truncate(self, size=None): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_truncate)), size) - def writable(self): - return self._jl_callmethod($(pyjl_methodnum(pyjlio_writable))) - def writelines(self, lines): - for line in lines: - self.write(line) - def __enter__(self): - return self - def __exit__(self, t, v, b): - self.close() - def __iter__(self): - return self - def __next__(self): - line = self.readline() - if line: - return line - else: - raise StopIteration -class BinaryIOValue(IOValueBase): - __slots__ = () - def detach(self): - raise ValueError("Cannot detach '{}'.".format(type(self))) - def read(self, size=-1): - return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_read)), size) - def read1(self, size=-1): - return self.read(size) - def readline(self, size=-1): - return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_readline)), size) - def readinto(self, b): - return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_readinto)), b) - def readinto1(self, b): - return self.readinto(b) - def write(self, b): - return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_write)), b) -class TextIOValue(IOValueBase): - __slots__ = () - @property - def encoding(self): - return "UTF-8" - @property - def errors(self): - return "strict" - def detach(self): - raise ValueError("Cannot detach '{}'.".format(type(self))) - def read(self, size=-1): - return self._jl_callmethod($(pyjl_methodnum(pyjltextio_read)), size) - def readline(self, size=-1): - return self._jl_callmethod($(pyjl_methodnum(pyjltextio_readline)), size) - 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) -del io -""", + $("\n"^(@__LINE__()-1)) + 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 + def closed(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_closed))) + def fileno(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_fileno))) + def flush(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_flush))) + def isatty(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_isatty))) + def readable(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_readable))) + def readlines(self, hint=-1): + lines = [] + total = 0 + while hint < 0 or total < hint: + line = self.readline() + if line: + lines.append(line) + total += len(line) + else: + break + return lines + def seek(self, offset, whence=0): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_seek)), offset, whence) + def seekable(self): + return True + def tell(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_tell))) + def truncate(self, size=None): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_truncate)), size) + def writable(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlio_writable))) + def writelines(self, lines): + for line in lines: + self.write(line) + def __enter__(self): + return self + def __exit__(self, t, v, b): + self.close() + def __iter__(self): + return self + def __next__(self): + line = self.readline() + if line: + return line + else: + raise StopIteration + class JlBinaryIO(JlIOBase): + __slots__ = () + def detach(self): + raise ValueError("Cannot detach '{}'.".format(type(self))) + def read(self, size=-1): + return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_read)), size) + def read1(self, size=-1): + return self.read(size) + def readline(self, size=-1): + return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_readline)), size) + def readinto(self, b): + return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_readinto)), b) + def readinto1(self, b): + return self.readinto(b) + def write(self, b): + return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_write)), b) + class JlTextIO(JlIOBase): + __slots__ = () + @property + def encoding(self): + return "UTF-8" + @property + def errors(self): + return "strict" + def detach(self): + raise ValueError("Cannot detach '{}'.".format(type(self))) + def read(self, size=-1): + return self._jl_callmethod($(pyjl_methodnum(pyjltextio_read)), size) + def readline(self, size=-1): + return self._jl_callmethod($(pyjl_methodnum(pyjltextio_readline)), size) + def write(self, s): + return self._jl_callmethod($(pyjl_methodnum(pyjltextio_write)), s) + import io + 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) @@ -345,4 +349,4 @@ Wrap `io` as a Python text IO object. """ pytextio(v::IO) = pyjl(pyjltextiotype, v) -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 787bd46f..00000000 --- a/src/JlWrap/iter.jl +++ /dev/null @@ -1,49 +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 2f636e08..00000000 --- a/src/JlWrap/module.jl +++ /dev/null @@ -1,37 +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 ebc5b2a6..00000000 --- a/src/JlWrap/number.jl +++ /dev/null @@ -1,246 +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 663b0087..00000000 --- a/src/JlWrap/raw.jl +++ /dev/null @@ -1,174 +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 - -function pyjlraw_call_nogil(self, args_::Py, kwargs_::Py) - if pylen(kwargs_) > 0 - args = pyconvert(Vector{Any}, args_) - kwargs = pyconvert(Dict{Symbol,Any}, kwargs_) - ans = pyjlraw(GIL.@unlock self(args...; kwargs...)) - elseif pylen(args_) > 0 - args = pyconvert(Vector{Any}, args_) - ans = pyjlraw(GIL.@unlock self(args...)) - else - ans = pyjlraw(GIL.@unlock 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): - '''Convert this to a juliacall.AnyValue.''' - return self._jl_callmethod($(pyjl_methodnum(pyjl))) - def _jl_call_nogil(self, *args, **kwargs): - '''Call this with the given arguments but with the GIL disabled. - - WARNING: This function must not interact with Python at all without re-acquiring - the GIL. - ''' - return self._jl_callmethod($(pyjl_methodnum(pyjlraw_call_nogil)), args, kwargs) -""", - @__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) diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl index 7b7b08e0..6ace3731 100644 --- a/src/JlWrap/set.jl +++ b/src/JlWrap/set.jl @@ -9,8 +9,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") @@ -79,55 +77,58 @@ function init_set() pybuiltins.exec( pybuiltins.compile( """ -$("\n"^(@__LINE__()-1)) -class SetValue(AnyValue): - __slots__ = () - def __bool__(self): - return bool(len(self)) - 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): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_remove)), value) - def difference(self, other): - return set(self).difference(other) - def intersection(self, other): - return set(self).intersection(other) - def symmetric_difference(self, other): - return set(self).symmetric_difference(other) - def union(self, other): - return set(self).union(other) - def isdisjoint(self, other): - return set(self).isdisjoint(other) - def issubset(self, other): - return set(self).issubset(other) - def issuperset(self, other): - return set(self).issuperset(other) - def difference_update(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_difference_update)), other) - def intersection_update(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_intersection_update)), other) - def symmetric_difference_update(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_symmetric_difference_update)), other) - def update(self, other): - return self._jl_callmethod($(pyjl_methodnum(pyjlset_update)), other) -import collections.abc -collections.abc.MutableSet.register(SetValue) -del collections -""", + $("\n"^(@__LINE__()-1)) + class JlSet(JlCollection): + __slots__ = () + 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 pop(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_pop))) + def remove(self, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_remove)), value) + def difference(self, other): + return set(self).difference(other) + def intersection(self, other): + return set(self).intersection(other) + def symmetric_difference(self, other): + return set(self).symmetric_difference(other) + def union(self, other): + return set(self).union(other) + def isdisjoint(self, other): + return set(self).isdisjoint(other) + def issubset(self, other): + return set(self).issubset(other) + def issuperset(self, other): + return set(self).issuperset(other) + def difference_update(self, other): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_difference_update)), other) + def intersection_update(self, other): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_intersection_update)), other) + def symmetric_difference_update(self, other): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_symmetric_difference_update)), other) + def update(self, other): + return self._jl_callmethod($(pyjl_methodnum(pyjlset_update)), other) + import collections.abc + 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) + +Py(x::AbstractSet) = pyjlset(x) diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl deleted file mode 100644 index 72a2f992..00000000 --- a/src/JlWrap/type.jl +++ /dev/null @@ -1,37 +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 3c71e5b9..e4699d65 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 @@ -122,43 +117,45 @@ function init_vector() pybuiltins.exec( pybuiltins.compile( """ -$("\n"^(@__LINE__()-1)) -class VectorValue(ArrayValue): - __slots__ = () - 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): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_insert)), index, value) - def append(self, value): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_append)), value) - def extend(self, values): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_extend)), values) - def pop(self, index=-1): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_pop)), index) - def remove(self, value): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_remove)), value) - def index(self, value): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_index)), value) - def count(self, value): - return self._jl_callmethod($(pyjl_methodnum(pyjlvector_count)), value) -import collections.abc -collections.abc.MutableSequence.register(VectorValue) -del collections -""", + $("\n"^(@__LINE__()-1)) + 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 __reversed__(self): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_reversed))) + def insert(self, index, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_insert)), index, value) + def append(self, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_append)), value) + def extend(self, values): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_extend)), values) + def pop(self, index=-1): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_pop)), index) + def remove(self, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_remove)), value) + def index(self, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_index)), value) + def count(self, value): + return self._jl_callmethod($(pyjl_methodnum(pyjlvector_count)), value) + import collections.abc + 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 46c89c89..e8c7597f 100644 --- a/src/PythonCall.jl +++ b/src/PythonCall.jl @@ -26,13 +26,7 @@ for k in [ :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, - :pyjlmoduletype, - :pyjlintegertype, - :pyjlrationaltype, - :pyjlrealtype, - :pyjlcomplextype, :pyjlsettype, - :pyjltypetype, ] @eval using .JlWrap: $k end diff --git a/src/Wrap/PyArray.jl b/src/Wrap/PyArray.jl index 7b1dc8d3..98cbaaee 100644 --- a/src/Wrap/PyArray.jl +++ b/src/Wrap/PyArray.jl @@ -1,37 +1,6 @@ -struct UnsafePyObject - ptr::C.PyPtr -end - - ispy(::PyArray) = true Py(x::PyArray) = x.py -for N in (missing, 1, 2) - for M in (missing, true, false) - for L in (missing, true, false) - for R in (true, false) - name = Symbol( - "Py", - M === missing ? "" : M ? "Mutable" : "Immutable", - L === missing ? "" : L ? "Linear" : "Cartesian", - R ? "Raw" : "", - N === missing ? "Array" : N == 1 ? "Vector" : "Matrix", - ) - name == :PyArray && continue - vars = Any[ - :T, - N === missing ? :N : N, - M === missing ? :M : M, - L === missing ? :L : L, - R ? :T : :R, - ] - @eval const $name{$(unique([v for v in vars if v isa Symbol])...)} = PyArray{$(vars...)} - @eval export $name - end - end - end -end - (::Type{A})( x; array::Bool = true, @@ -116,38 +85,29 @@ function pyarray_make( ::Type{A}, x::Py, info::PyArraySource, - ::Type{PyArray{T0,N0,M0,L0,R0}} = Utils._type_lb(A), - ::Type{PyArray{T1,N1,M1,L1,R1}} = Utils._type_ub(A), -) where {A<:PyArray,T0,N0,M0,L0,R0,T1,N1,M1,L1,R1} - # R (buffer eltype) + ::Type{PyArray{T0,N0,F0}} = Utils._type_lb(A), + ::Type{PyArray{T1,N1,F1}} = Utils._type_ub(A), +) where {A<:PyArray,T0,N0,F0,T1,N1,F1} + # T (eltype) - checked against R (ptr eltype) R′ = pyarray_get_R(info)::DataType - if R0 == R1 - R = R1 - R == R′ || error("incorrect R, got $R, should be $R′") - # elseif T0 == T1 && T1 in (Bool, Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128, Float16, Float32, Float64, ComplexF16, ComplexF32, ComplexF64) - # R = T1 - # R == R′ || error("incorrect R, got $R, should be $R′") - # R <: R1 || error("R out of bounds, got $R, should be <: $R1") - # R >: R0 || error("R out of bounds, got $R, should be >: $R0") - else - R = R′ - end - # ptr - ptr = pyarray_get_ptr(info, R)::Ptr{R} - # T (eltype) if T0 == T1 T = T1 - pyarray_check_T(T, R) + R = pyarray_get_R(T) + R′ == R || error("eltype $T is incompatible with array data of type $R′") else - T = pyarray_get_T(R, T0, T1)::DataType - T <: T1 || error("T out of bounds, got $T, should be <: $T1") - T >: T0 || error("T out of bounds, got $T, should be >: $T0") + T = pyarray_get_T(R′, T0, T1)::DataType + T <: T1 || error("computed eltype $T out of bounds, should be <: $T1") + T >: T0 || error("computed eltype $T out of bounds, should be >: $T0") + R = pyarray_get_R(T) + R′ == R || error("computed eltype $T is incompatible with array data of type $R′") end + # ptr + ptr = pyarray_get_ptr(info)::Ptr{Cvoid} # N (ndims) N′ = pyarray_get_N(info)::Int if N0 == N1 N = N1 isa Int ? N1 : Int(N1) - N == N′ || error("incorrect N, got $N, should be $N′") + N == N′ || error("number of dimensions $N incorrect, expecting $N′") else N = N′ end @@ -155,28 +115,28 @@ function pyarray_make( size = pyarray_get_size(info, Val(N))::NTuple{N,Int} # strides strides = pyarray_get_strides(info, Val(N), R, size)::NTuple{N,Int} - # M (mutable) - # TODO: if M==false, we don't need to compute M′ - M′ = pyarray_get_M(info)::Bool - if M0 == M1 - M = M1 isa Bool ? M1 : Bool(M1) - M && !M′ && error("incorrect M, got $M, should be $M′") - else - M = M′ - end - # L (linearly indexable) - # TODO: if L==false, we don't need to compute L′ - L′ = N < 2 || strides == Utils.size_to_fstrides(strides[1], size) - if L0 == L1 - L = L1 isa Bool ? L1 : Bool(L1) - L && !L′ && error("incorrect L, got $L, should be $L′") + # F (flags) + M = pyarray_get_M(info)::Bool + L = N < 2 || strides == Utils.size_to_fstrides(strides[1], size) + C = L && (N == 0 || strides[1] == sizeof(R)) + Flist = Symbol[] + if F0 == F1 + F = F1::Tuple{Vararg{Symbol}} + (:mutable in F) && (!M) && error(":mutable flag given but array is not mutable") + (:linear in F) && (!L) && error(":linear flag given but array is not evenly spaced") + (:contiguous in F) && + (!C) && + error(":contiguous flag given but array is not contiguous") else - L = L′ + M && push!(Flist, :mutable) + L && push!(Flist, :linear) + C && push!(Flist, :contiguous) + F = Tuple(Flist) end # handle handle = pyarray_get_handle(info) # done - arr = PyArray{T,N,M,L,R}(Val(:new), ptr, size, strides, x, handle) + arr = PyArray{T,N,F}(Val(:new), ptr, size, strides, x, handle) return pyconvert_return(arr) end @@ -367,7 +327,7 @@ function pyarray_get_R(src::PyArraySource_ArrayInterface) return R end -pyarray_get_ptr(src::PyArraySource_ArrayInterface, ::Type{R}) where {R} = Ptr{R}(src.ptr) +pyarray_get_ptr(src::PyArraySource_ArrayInterface) = src.ptr pyarray_get_N(src::PyArraySource_ArrayInterface) = Int(@py jllen(@jl(src.dict)["shape"])) @@ -490,8 +450,8 @@ function pyarray_get_R(src::PyArraySource_ArrayStruct) @assert false end -function pyarray_get_ptr(src::PyArraySource_ArrayStruct, ::Type{R}) where {R} - return Ptr{R}(src.info.data) +function pyarray_get_ptr(src::PyArraySource_ArrayStruct) + src.info.data end function pyarray_get_N(src::PyArraySource_ArrayStruct) @@ -564,7 +524,7 @@ const PYARRAY_BUFFERFORMAT_TO_TYPE = let c = Utils.islittleendian() ? '<' : '>' "$(c)d" => Cdouble, "?" => Bool, "P" => Ptr{Cvoid}, - "O" => UnsafePyObject, + "O" => C.PyPtr, "=e" => Float16, "=f" => Float32, "=d" => Float64, @@ -582,7 +542,7 @@ function pyarray_get_R(src::PyArraySource_Buffer) return ptr == C_NULL ? UInt8 : pyarray_bufferformat_to_type(String(ptr)) end -pyarray_get_ptr(src::PyArraySource_Buffer, ::Type{R}) where {R} = Ptr{R}(src.buf.buf[!]) +pyarray_get_ptr(src::PyArraySource_Buffer) = src.buf.buf[!] pyarray_get_N(src::PyArraySource_Buffer) = Int(src.buf.ndim[]) @@ -620,93 +580,100 @@ Base.length(x::PyArray) = x.length Base.size(x::PyArray) = x.size -Utils.ismutablearray(x::PyArray{T,N,M,L,R}) where {T,N,M,L,R} = M +Utils.ismutablearray(x::PyArray{T,N,F}) where {T,N,F} = (:mutable in F) -Base.IndexStyle(::Type{PyArray{T,N,M,L,R}}) where {T,N,M,L,R} = - L ? Base.IndexLinear() : Base.IndexCartesian() +Base.IndexStyle(::Type{PyArray{T,N,F}}) where {T,N,F} = + (:linear in F) ? Base.IndexLinear() : Base.IndexCartesian() -Base.unsafe_convert(::Type{Ptr{T}}, x::PyArray{T,N,M,L,T}) where {T,N,M,L} = x.ptr +Base.unsafe_convert(::Type{Ptr{T}}, x::PyArray{T}) where {T} = + pyarray_get_R(T) == T ? Ptr{T}(x.ptr) : error("") -Base.elsize(::Type{PyArray{T,N,M,L,T}}) where {T,N,M,L} = sizeof(T) +Base.elsize(::Type{<:PyArray{T}}) where {T} = pyarray_get_R(T) == T ? sizeof(T) : error("") -Base.strides(x::PyArray{T,N,M,L,R}) where {T,N,M,L,R} = +function Base.strides(x::PyArray{T}) where {T} + R = pyarray_get_R(T) if all(mod.(x.strides, sizeof(R)) .== 0) div.(x.strides, sizeof(R)) else error("strides are not a multiple of element size") end - -function Base.showarg(io::IO, x::PyArray{T,N}, toplevel::Bool) where {T,N} - toplevel || print(io, "::") - print(io, "PyArray{") - show(io, T) - print(io, ", ", N, "}") - return end @propagate_inbounds Base.getindex(x::PyArray{T,N}, i::Vararg{Int,N}) where {T,N} = pyarray_getindex(x, i...) -@propagate_inbounds Base.getindex(x::PyArray{T,N,M,true}, i::Int) where {T,N,M} = - pyarray_getindex(x, i) -@propagate_inbounds Base.getindex(x::PyArray{T,1,M,true}, i::Int) where {T,M} = - pyarray_getindex(x, i) +@propagate_inbounds Base.getindex(x::PyArray, i::Int) = pyarray_getindex(x, i) -@propagate_inbounds function pyarray_getindex(x::PyArray, i...) +@propagate_inbounds function pyarray_getindex(x::PyArray{T}, i...) where {T} @boundscheck checkbounds(x, i...) - pyarray_load(eltype(x), x.ptr + pyarray_offset(x, i...)) + R = pyarray_get_R(T) + pyarray_load(T, Ptr{R}(x.ptr + pyarray_offset(x, i...))) end -@propagate_inbounds Base.setindex!(x::PyArray{T,N,true}, v, i::Vararg{Int,N}) where {T,N} = +@propagate_inbounds Base.setindex!(x::PyArray{T,N}, v, i::Vararg{Int,N}) where {T,N} = pyarray_setindex!(x, v, i...) -@propagate_inbounds Base.setindex!(x::PyArray{T,N,true,true}, v, i::Int) where {T,N} = - pyarray_setindex!(x, v, i) -@propagate_inbounds Base.setindex!(x::PyArray{T,1,true,true}, v, i::Int) where {T} = - pyarray_setindex!(x, v, i) +@propagate_inbounds Base.setindex!(x::PyArray, v, i::Int) = pyarray_setindex!(x, v, i) -@propagate_inbounds function pyarray_setindex!(x::PyArray{T,N,true}, v, i...) where {T,N} +@propagate_inbounds function pyarray_setindex!(x::PyArray{T,N,F}, v, i...) where {T,N,F} + (:mutable in F) || error("array is immutable") @boundscheck checkbounds(x, i...) - pyarray_store!(x.ptr + pyarray_offset(x, i...), convert(T, v)) + R = pyarray_get_R(T) + pyarray_store!(T, Ptr{R}(x.ptr + pyarray_offset(x, i...)), convert(T, v)::T) x end -pyarray_offset(x::PyArray{T,N,M,true}, i::Int) where {T,N,M} = - N == 0 ? 0 : (i - 1) * x.strides[1] -pyarray_offset(x::PyArray{T,1,M,true}, i::Int) where {T,M} = (i - 1) .* x.strides[1] -pyarray_offset(x::PyArray{T,N}, i::Vararg{Int,N}) where {T,N} = sum((i .- 1) .* x.strides) -pyarray_offset(x::PyArray{T,0}) where {T} = 0 +function pyarray_offset(x::PyArray{T,N,F}, i::Int) where {T,N,F} + if N == 0 + 0 + elseif (:contiguous in F) + (i - 1) * sizeof(pyarray_get_R(T)) + elseif (N == 1) || (:linear in F) + (i - 1) * x.strides[1] + else + # convert i to cartesian indices + # there's no public function for this :( + j = Base._to_subscript_indices(x, i) + sum((j .- 1) .* x.strides) + end +end + +function pyarray_offset(x::PyArray{T,N,F}, i::Vararg{Int,N}) where {T,N,F} + sum((i .- 1) .* x.strides) +end function pyarray_load(::Type{T}, p::Ptr{R}) where {T,R} if R == T unsafe_load(p) - elseif R == UnsafePyObject + elseif R == C.PyPtr u = unsafe_load(p) - o = u.ptr == C_NULL ? pynew(Py(nothing)) : pynew(incref(u.ptr)) + o = u == C_NULL ? pynew(Py(nothing)) : pynew(incref(u)) T == Py ? o : pyconvert(T, o) else convert(T, unsafe_load(p)) end end -function pyarray_store!(p::Ptr{R}, x::T) where {R,T} +function pyarray_store!(::Type{T}, p::Ptr{R}, x::T) where {R,T} if R == T unsafe_store!(p, x) - elseif R == UnsafePyObject + elseif R == C.PyPtr @autopy x begin decref(unsafe_load(p).ptr) - unsafe_store!(p, UnsafePyObject(getptr(incref(x_)))) + unsafe_store!(p, getptr(incref(x_))) end else unsafe_store!(p, convert(R, x)) end end -function pyarray_get_T(::Type{R}, ::Type{T0}, ::Type{T1}) where {R,T0,T1} - if R == UnsafePyObject +@generated function pyarray_get_T(::Type{R}, ::Type{T0}, ::Type{T1}) where {R,T0,T1} + if R == C.PyPtr if T0 <: Py <: T1 Py else - T1 + error("impossible") end + elseif R <: Tuple + error("not implemented") elseif T0 <: R <: T1 R else @@ -714,16 +681,38 @@ function pyarray_get_T(::Type{R}, ::Type{T0}, ::Type{T1}) where {R,T0,T1} end end -function pyarray_check_T(::Type{T}, ::Type{R}) where {T,R} - if R == UnsafePyObject - nothing - elseif T == R - nothing - elseif T <: Number && R <: Number - nothing - elseif T <: AbstractString && R <: AbstractString - nothing +@generated function pyarray_get_R(::Type{T}) where {T} + if ( + T == Bool || + T == Int8 || + T == Int16 || + T == Int32 || + T == Int64 || + T == UInt8 || + T == UInt16 || + T == UInt32 || + T == UInt64 || + T == Float16 || + T == Float32 || + T == Float64 || + T == Complex{Float16} || + T == Complex{Float32} || + T == Complex{Float16} + ) + T + elseif isconcretetype(T) && + isbitstype(T) && + ( + T <: NumpyDates.InlineDateTime64 || + T <: NumpyDates.InlineTimeDelta64 || + T <: Ptr + ) + T + elseif T == Py + C.PyPtr + elseif T <: Tuple + Tuple{map(pyarray_get_R, T.parameters)...} else - error("invalid eltype T=$T for raw eltype R=$R") + error("unsupported eltype $T") end end diff --git a/test/Compat.jl b/test/Compat.jl index 00c93ac6..ed230a71 100644 --- a/test/Compat.jl +++ b/test/Compat.jl @@ -71,7 +71,7 @@ end @testitem "Serialization.jl" begin using Serialization @testset "Py" begin - for x in Py[ + @testset for x in Py[ Py(123), Py(1.23), Py("hello"), diff --git a/test/Core.jl b/test/Core.jl index 8f2e428a..16574a4a 100644 --- a/test/Core.jl +++ b/test/Core.jl @@ -220,6 +220,34 @@ @test Base.Docs.getdoc(pybuiltins.int) isa Markdown.MD @test Base.Docs.getdoc(PythonCall.PyNULL) === nothing end + @testset "comparisons" begin + @testset "Py vs Py" begin + # == + @test Py(1) == Py(1) + @test !(Py(1) == Py(2)) + @test !(Py(1) == Py(0)) + # != + @test Py(2) != Py(1) + @test Py(2) != Py(3) + @test !(Py(2) != Py(2)) + # < + @test Py(3) < Py(4) + @test !(Py(3) < Py(3)) + @test !(Py(3) < Py(2)) + # <= + @test Py(4) <= Py(5) + @test Py(4) <= Py(4) + @test !(Py(4) <= Py(3)) + # > + @test Py(5) > Py(4) + @test !(Py(5) > Py(5)) + @test !(Py(5) > Py(6)) + # >= + @test Py(5) >= Py(4) + @test Py(5) >= Py(5) + @test !(Py(5) >= Py(6)) + end + end end @testitem "iter" begin @@ -785,13 +813,13 @@ end @testitem "Base.jl" begin @testset "broadcast" begin # Py always broadcasts as a scalar - x = [1 2; 3 4] .+ Py(1) - @test isequal(x, [Py(2) Py(3); Py(4) Py(5)]) - x = Py("foo") .* [1 2; 3 4] + x = Py.([1 2; 3 4]) .+ Py(1) + @test isequal(x, Py.([2 3; 4 5])) + x = Py("foo") .* Py.([1 2; 3 4]) @test isequal(x, [Py("foo") Py("foofoo"); Py("foofoofoo") Py("foofoofoofoo")]) # this previously treated the list as a shape (2,) object # but now tries to do `1 + [1, 2]` which properly fails - @test_throws PyException [1 2; 3 4] .+ pylist([1, 2]) + @test_throws PyException Py.([1 2; 3 4]) .+ pylist([1, 2]) end @testset "showable" begin @test showable(MIME("text/plain"), Py(nothing)) diff --git a/test/GC.jl b/test/GC.jl index 475c547c..f180cfaf 100644 --- a/test/GC.jl +++ b/test/GC.jl @@ -1,8 +1,11 @@ @testitem "GC.gc()" begin let pyobjs = map(pylist, 1:100) - PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs - finalize(obj) + PythonCall.GIL.@unlock begin + Threads.@threads for obj in pyobjs + finalize(obj) + end + Threads.nthreads() > 1 && @test !isempty(PythonCall.GC.QUEUE.items) end end Threads.nthreads() > 1 && @@ -15,8 +18,11 @@ end @testitem "GC.GCHook" begin let pyobjs = map(pylist, 1:100) - PythonCall.GIL.@unlock Threads.@threads for obj in pyobjs - finalize(obj) + PythonCall.GIL.@unlock begin + Threads.@threads for obj in pyobjs + finalize(obj) + end + Threads.nthreads() > 1 && @test !isempty(PythonCall.GC.QUEUE.items) end end Threads.nthreads() > 1 && diff --git a/test/GIL.jl b/test/GIL.jl index ca1f6405..57e1fca5 100644 --- a/test/GIL.jl +++ b/test/GIL.jl @@ -3,7 +3,7 @@ # GIL, these can happen in parallel if Julia has at least 2 threads. function threaded_sleep() PythonCall.GIL.unlock() do - Threads.@threads for i = 1:2 + Threads.@threads :static for i = 1:2 PythonCall.GIL.lock() do pyimport("time").sleep(1) end @@ -24,7 +24,7 @@ end # This calls Python's time.sleep(1) twice concurrently. Since sleep() unlocks the # GIL, these can happen in parallel if Julia has at least 2 threads. function threaded_sleep() - PythonCall.GIL.@unlock Threads.@threads for i = 1:2 + PythonCall.GIL.@unlock Threads.@threads :static for i = 1:2 PythonCall.GIL.@lock pyimport("time").sleep(1) end end diff --git a/test/JlWrap.jl b/test/JlWrap.jl index 56bf0e8e..b656ab91 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -24,16 +24,19 @@ 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,7 +155,7 @@ @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 @@ -153,7 +163,7 @@ @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,41 +216,213 @@ @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 - @test_skip pyis(pyjl(Foo(1))._jl_help(), nothing) - @test_skip pyis(pyjl(Foo(1))._jl_help(mime = "text/plain"), nothing) + using REPL + 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 bits don't always match + @test mod(pyhash(pyjlcollection(c.x)), UInt32) >> 16 == mod(hash(c.x), UInt32) >> 16 + 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) @@ -257,7 +439,7 @@ end @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]) + 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,7 +449,7 @@ end end @testset "setitem" begin x = [0 0; 0 0] - y = pyjl(x) + y = pyjlarray(x) y[0, 0] = 1 @test x == [1 0; 0 0] y[0, 1] = 2 @@ -285,7 +467,7 @@ end 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) @@ -296,14 +478,14 @@ end @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) @@ -314,7 +496,7 @@ end @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 pyconvert(Int, z) == Base.cconvert(Cint, 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, stdout isa Base.TTY), + ] + # 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 = 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,38 +1180,38 @@ 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]) + 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]) + 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]) + 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] 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] end @testset "clear" begin - x = pyjl([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] 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] x.insert(2, 5) @@ -512,7 +1223,7 @@ end @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] x.append(5.0) @@ -522,7 +1233,7 @@ end @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])) @@ -531,7 +1242,7 @@ end @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 pyeq(Bool, x.pop(0), 1) @@ -543,7 +1254,7 @@ end @test_throws PyException x.pop(10) end @testset "remove" begin - x = pyjl([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] @@ -553,7 +1264,7 @@ end @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 +1277,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/Project.toml b/test/Project.toml index 3f088776..50bccf9d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/PyMacro.jl b/test/PyMacro.jl index 2b5bd4cf..7d35273a 100644 --- a/test/PyMacro.jl +++ b/test/PyMacro.jl @@ -197,13 +197,13 @@ @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 diff --git a/test/Wrap.jl b/test/Wrap.jl index 9e23f8a3..e3aec817 100644 --- a/test/Wrap.jl +++ b/test/Wrap.jl @@ -1,10 +1,10 @@ @testitem "PyArray" begin x = pyimport("array").array("i", pylist([1, 2, 3])) y = PyArray(x) - z = PyArray{Cint,1,false,false,Cint}(x) + z = PyArray{Cint,1,()}(x) @testset "construct" begin - @test y isa PyArray{Cint,1,true,true,Cint} - @test z isa PyArray{Cint,1,false,false,Cint} + @test y isa PyArray{Cint,1,(:mutable,:linear,:contiguous)} + @test z isa PyArray{Cint,1,()} @test PythonCall.ispy(y) @test PythonCall.ispy(z) @test Py(y) === x @@ -29,9 +29,10 @@ @testset "elsize" begin @test Base.elsize(y) === sizeof(Cint) @test Base.elsize(z) === sizeof(Cint) - @test Base.elsize(PyArray{Cint,1,true,true,Cint}) === sizeof(Cint) - @test Base.elsize(PyArray{Cint,1,false,false,Cint}) === sizeof(Cint) - @test_throws Exception elsize(PyArray{Cint,1,true,false,Cchar}) + @test Base.elsize(PyArray{UInt8}) === sizeof(UInt8) + @test Base.elsize(PyArray{UInt16,2}) === sizeof(UInt16) + @test Base.elsize(PyArray{UInt32,3,(:mutable,)}) === sizeof(UInt32) + @test_throws Exception elsize(PyArray{Py,1,()}) end @testset "getindex" begin @test_throws BoundsError y[0] diff --git a/test/runtests.jl b/test/runtests.jl index 115648be..f95534d9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,11 @@ using TestItemRunner +# if you run tests in a conda environment, these env vars cause the aqua persistent tasks test to error +if haskey(ENV, "CONDA_PREFIX") + delete!(ENV, "SSL_CERT_FILE") + delete!(ENV, "SSL_CERT_DIR") +end + @testmodule Setup begin using PythonCall # test if we are in CI