From 7e8d373f1ce8d02a06f0d46217f0621f12c91a71 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 08:53:43 +0100
Subject: [PATCH 01/39] change julia wrapper hierarchy

---
 docs/src/conversion-to-julia.md  |  2 +-
 docs/src/conversion-to-python.md |  4 ++--
 docs/src/juliacall-reference.md  | 22 +++++++++++-----------
 docs/src/juliacall.md            |  2 +-
 pytest/test_all.py               |  2 +-
 src/JlWrap/C.jl                  |  6 +++---
 src/JlWrap/JlWrap.jl             |  2 +-
 src/JlWrap/any.jl                | 10 +++++-----
 src/JlWrap/array.jl              |  2 +-
 src/JlWrap/base.jl               |  8 ++++----
 src/JlWrap/callback.jl           |  2 +-
 src/JlWrap/dict.jl               |  2 +-
 src/JlWrap/io.jl                 |  2 +-
 src/JlWrap/iter.jl               |  2 +-
 src/JlWrap/module.jl             |  4 ++--
 src/JlWrap/number.jl             |  2 +-
 src/JlWrap/raw.jl                |  6 +++---
 src/JlWrap/set.jl                |  2 +-
 src/JlWrap/type.jl               |  2 +-
 19 files changed, 42 insertions(+), 42 deletions(-)

diff --git a/docs/src/conversion-to-julia.md b/docs/src/conversion-to-julia.md
index e08a5d19..3cf71459 100644
--- a/docs/src/conversion-to-julia.md
+++ b/docs/src/conversion-to-julia.md
@@ -11,7 +11,7 @@ From Python, the arguments to a Julia function will be converted according to th
 | From                                                                                                         | To                                                          |
 | :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
 | **Top priority (wrapped values).**                                                                           |                                                             |
-| `juliacall.AnyValue`                                                                                         | `Any`                                                       |
+| `juliacall.Jl`                                                                                         | `Any`                                                       |
 | **Very high priority (arrays).**                                                                             |                                                             |
 | Objects satisfying the buffer or array interface (inc. `bytes`, `bytearray`, `array.array`, `numpy.ndarray`) | `PyArray`                                                   |
 | **High priority (canonical conversions).**                                                                   |                                                             |
diff --git a/docs/src/conversion-to-python.md b/docs/src/conversion-to-python.md
index bb38ec5a..d95ad269 100644
--- a/docs/src/conversion-to-python.md
+++ b/docs/src/conversion-to-python.md
@@ -30,7 +30,7 @@ From Python, this occurs when converting the return value of a Julia function.
 | `IO`                                                                | `juliacall.BufferedIOValue`                             |
 | `Module`                                                            | `juliacall.ModuleValue`                                 |
 | `Type`                                                              | `juliacall.TypeValue`                                   |
-| Anything else                                                       | `juliacall.AnyValue`                                    |
+| Anything else                                                       | `juliacall.Jl`                                    |
 
 See [here](@ref julia-wrappers) for an explanation of the `juliacall.*Value` wrapper types.
 
@@ -46,7 +46,7 @@ PythonCall.ispy
 ```
 
 Alternatively, if you define a wrapper type (a subtype of
-[`juliacall.AnyValue`](#juliacall.AnyValue)) then you may instead define `pyjltype(::T)` to
+[`juliacall.Jl`](#juliacall.Jl)) then you may instead define `pyjltype(::T)` to
 be that type.
 
 ```@docs
diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md
index c62a480b..6a1b40c5 100644
--- a/docs/src/juliacall-reference.md
+++ b/docs/src/juliacall-reference.md
@@ -45,19 +45,19 @@ 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
+Python to some [`Jl`](#juliacall.Jl) object, which wraps the original value, but
 giving it a Pythonic interface.
 
-Subclasses of [`AnyValue`](#juliacall.AnyValue) provide additional Python semantics. For
+Subclasses of [`Jl`](#juliacall.Jl) 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`:
+"Julia-only" interface, documented below. These types all inherit from `JlBase`:
 
-- `ValueBase`
+- `JlBase`
   - [`RawValue`](#juliacall.RawValue)
-  - [`AnyValue`](#juliacall.AnyValue)
+  - [`Jl`](#juliacall.Jl)
     - [`NumberValue`](#juliacall.NumberValue)
       - `ComplexValue`
       - `RealValue`
@@ -74,7 +74,7 @@ There is also a [`RawValue`](#juliacall.RawValue) object, which gives a stricter
     - [`TypeValue`](#juliacall.TypeValue)
 
 `````@customdoc
-juliacall.AnyValue - Class
+juliacall.Jl - Class
 
 Wraps any Julia object, giving it some basic Python semantics. Subtypes provide extra
 semantics.
@@ -84,7 +84,7 @@ 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`.
+immutables) or a subtype of `Jl`.
 
 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
@@ -181,7 +181,7 @@ There are also subtypes `BinaryIOValue` and `TextIOValue`, which are subclasses
 juliacall.ModuleValue - Class
 This wraps any Julia `Module` value.
 
-It is the same as [`AnyValue`](#juliacall.AnyValue) except for one additional convenience
+It is the same as [`Jl`](#juliacall.Jl) except for one additional convenience
 method:
 - `seval([module=self], code)`: Evaluates the given code (a string) in the given module.
 `````
@@ -191,7 +191,7 @@ juliacall.TypeValue - Class
 
 This wraps any Julia `Type` value.
 
-It is the same as [`AnyValue`](#juliacall.AnyValue) except that indexing is used to access
+It is the same as [`Jl`](#juliacall.Jl) except that indexing is used to access
 Julia's "curly" syntax for specifying parametric types:
 
 ```python
@@ -208,13 +208,13 @@ 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,
+This is very similar to [`Jl`](#juliacall.Jl) 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
+- `_jl_any()`: Convert to a [`Jl`](#juliacall.Jl) (or subclass). (See also
   [`pyjl`](@ref).)
 `````
diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md
index a16b71f2..d111a0c9 100644
--- a/docs/src/juliacall.md
+++ b/docs/src/juliacall.md
@@ -64,7 +64,7 @@ jl.cor(x, y)
 ```
 
 What to read next:
-- The main functionality of this package is in `AnyValue` objects, which represent Julia
+- The main functionality of this package is in `Jl` objects, which represent Julia
   objects, [documented here](@ref julia-wrappers).
 - If you need to install Julia packages, [read here](@ref julia-deps).
 - When you call a Julia function, such as `jl.rand(...)` in the above example, its
diff --git a/pytest/test_all.py b/pytest/test_all.py
index c6cff009..e531e321 100644
--- a/pytest/test_all.py
+++ b/pytest/test_all.py
@@ -14,7 +14,7 @@ def test_convert():
     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 isinstance(y, juliacall.Jl)
         assert jl.isa(y, t)
 
 def test_interactive():
diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl
index e90dc478..1451a1dd 100644
--- a/src/JlWrap/C.jl
+++ b/src/JlWrap/C.jl
@@ -251,7 +251,7 @@ function _pyjl_deserialize(t::C.PyPtr, v::C.PyPtr)
     end
 end
 
-const _pyjlbase_name = "juliacall.ValueBase"
+const _pyjlbase_name = "juliacall.JlBase"
 const _pyjlbase_type = fill(C.PyTypeObject())
 const _pyjlbase_isnull_name = "_jl_isnull"
 const _pyjlbase_callmethod_name = "_jl_callmethod"
@@ -311,7 +311,7 @@ function init_c()
     o = PyJuliaBase_Type[] = C.PyPtr(pointer(_pyjlbase_type))
     if C.PyType_Ready(o) == -1
         C.PyErr_Print()
-        error("Error initializing 'juliacall.ValueBase'")
+        error("Error initializing 'juliacall.JlBase'")
     end
 end
 
@@ -344,7 +344,7 @@ end
 
 PyJuliaValue_New(t::C.PyPtr, @nospecialize(v)) = begin
     if C.PyType_IsSubtype(t, PyJuliaBase_Type[]) != 1
-        C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.ValueBase'")
+        C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.JlBase'")
         return C.PyNULL
     end
     o = C.PyObject_CallObject(t, C.PyNULL)
diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index 498cc382..7614b053 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
 
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 92a53a2b..cddec22c 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -191,7 +191,7 @@ function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class AnyValue(ValueBase):
+    class Jl(JlBase):
         __slots__ = ()
         def __repr__(self):
             if self._jl_isnull():
@@ -210,7 +210,7 @@ function init_any()
                 return self._jl_callmethod($(pyjl_methodnum(pyjlany_getattr)), k)
         def __setattr__(self, k, v):
             try:
-                ValueBase.__setattr__(self, k, v)
+                JlBase.__setattr__(self, k, v)
             except AttributeError:
                 if k.startswith("__") and k.endswith("__"):
                     raise
@@ -218,7 +218,7 @@ function init_any()
                 return
             self._jl_callmethod($(pyjl_methodnum(pyjlany_setattr)), k, v)
         def __dir__(self):
-            return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlany_dir)))
+            return JlBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlany_dir)))
         def __call__(self, *args, **kwargs):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_call)), args, kwargs)
         def __bool__(self):
@@ -325,7 +325,7 @@ function init_any()
         def _repr_mimebundle_(self, include=None, exclude=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude)
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjlanytype, jl.AnyValue)
+    pycopy!(pyjlanytype, jl.Jl)
 end
 
 """
@@ -348,7 +348,7 @@ export pyjl
 """
     pyjltype(x)
 
-The subtype of `juliacall.AnyValue` which the Julia object `x` is wrapped as by `pyjl(x)`.
+The subtype of `juliacall.Jl` which the Julia object `x` is wrapped as by `pyjl(x)`.
 
 Overload `pyjltype(::T)` to define a custom conversion for your type `T`.
 """
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 2cb33bbc..cb27c07b 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -295,7 +295,7 @@ function init_array()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class ArrayValue(AnyValue):
+    class ArrayValue(JlBase):
         __slots__ = ()
         _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info))
         @property
diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl
index 08793e63..6d97412c 100644
--- a/src/JlWrap/base.jl
+++ b/src/JlWrap/base.jl
@@ -9,7 +9,7 @@ pyjl(t, v) = pynew(errcheck(@autopy t Cjl.PyJuliaValue_New(getptr(t_), v)))
 """
     pyisjl(x)
 
-Test whether `x` is a wrapped Julia value, namely an instance of `juliacall.ValueBase`.
+Test whether `x` is a wrapped Julia value, namely an instance of `juliacall.JlBase`.
 """
 pyisjl(x) = pytypecheck(x, pyjlbasetype)
 export pyisjl
@@ -18,7 +18,7 @@ pyjlisnull(x) = @autopy x begin
     if pyisjl(x_)
         Cjl.PyJuliaValue_IsNull(getptr(x_))
     else
-        error("Expecting a 'juliacall.ValueBase', got a '$(pytype(x_).__name__)'")
+        error("Expecting a 'juliacall.JlBase', got a '$(pytype(x_).__name__)'")
     end
 end
 
@@ -38,11 +38,11 @@ export pyjlvalue
 
 function init_base()
     setptr!(pyjlbasetype, incref(Cjl.PyJuliaBase_Type[]))
-    pyjuliacallmodule.ValueBase = pyjlbasetype
+    pyjuliacallmodule.JlBase = pyjlbasetype
 
     # conversion rule
     priority = PYCONVERT_PRIORITY_WRAP
-    pyconvert_add_rule("juliacall:ValueBase", Any, pyconvert_rule_jlvalue, priority)
+    pyconvert_add_rule("juliacall:JlBase", Any, pyconvert_rule_jlvalue, priority)
 end
 
 pyconvert_rule_jlvalue(::Type{T}, x::Py) where {T} = pyconvert_tryconvert(T, _pyjl_getvalue(x))
diff --git a/src/JlWrap/callback.jl b/src/JlWrap/callback.jl
index c25c10dd..1c82b455 100644
--- a/src/JlWrap/callback.jl
+++ b/src/JlWrap/callback.jl
@@ -39,7 +39,7 @@ function init_callback()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class CallbackValue(ValueBase):
+    class CallbackValue(JlBase):
         __slots__ = ()
         def __repr__(self):
             if self._jl_isnull():
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index 4d01674a..6df2cc0b 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -35,7 +35,7 @@ function init_dict()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class DictValue(AnyValue):
+    class DictValue(JlBase):
         __slots__ = ()
         _jl_undefined_ = object()
         def __bool__(self):
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index 85785fee..1dfe3e6c 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -205,7 +205,7 @@ function init_io()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class IOValueBase(AnyValue):
+    class IOValueBase(JlBase):
         __slots__ = ()
         def close(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlio_close)))
diff --git a/src/JlWrap/iter.jl b/src/JlWrap/iter.jl
index 90d24537..499afcf7 100644
--- a/src/JlWrap/iter.jl
+++ b/src/JlWrap/iter.jl
@@ -29,7 +29,7 @@ function init_iter()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class IteratorValue(AnyValue):
+    class IteratorValue(JlBase):
         __slots__ = ()
         def __iter__(self):
             return self
diff --git a/src/JlWrap/module.jl b/src/JlWrap/module.jl
index 8d879ae5..02257658 100644
--- a/src/JlWrap/module.jl
+++ b/src/JlWrap/module.jl
@@ -17,10 +17,10 @@ function init_module()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class ModuleValue(AnyValue):
+    class ModuleValue(JlBase):
         __slots__ = ()
         def __dir__(self):
-            return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlmodule_dir)))
+            return JlBase.__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__)
diff --git a/src/JlWrap/number.jl b/src/JlWrap/number.jl
index 1ed4013c..0f43c64a 100644
--- a/src/JlWrap/number.jl
+++ b/src/JlWrap/number.jl
@@ -87,7 +87,7 @@ function init_number()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class NumberValue(AnyValue):
+    class NumberValue(JlBase):
         __slots__ = ()
         def __bool__(self):
             return not self._jl_callmethod($(pyjl_methodnum(pyjlnumber_op(iszero))))
diff --git a/src/JlWrap/raw.jl b/src/JlWrap/raw.jl
index a531df37..1fd56330 100644
--- a/src/JlWrap/raw.jl
+++ b/src/JlWrap/raw.jl
@@ -85,7 +85,7 @@ function init_raw()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class RawValue(ValueBase):
+    class RawValue(JlBase):
         __slots__ = ()
         def __repr__(self):
             if self._jl_isnull():
@@ -104,7 +104,7 @@ function init_raw()
                 return self._jl_callmethod($(pyjl_methodnum(pyjlraw_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
@@ -112,7 +112,7 @@ function init_raw()
                 return
             self._jl_callmethod($(pyjl_methodnum(pyjlraw_setattr)), k, v)
         def __dir__(self):
-            return ValueBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlraw_dir)))
+            return JlBase.__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):
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index 38c9b82d..2694d13a 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -77,7 +77,7 @@ function init_set()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class SetValue(AnyValue):
+    class SetValue(JlBase):
         __slots__ = ()
         def __bool__(self):
             return bool(len(self))
diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl
index ef079f5c..050605e4 100644
--- a/src/JlWrap/type.jl
+++ b/src/JlWrap/type.jl
@@ -15,7 +15,7 @@ function init_type()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class TypeValue(AnyValue):
+    class TypeValue(JlBase):
         __slots__ = ()
         def __getitem__(self, k):
             return self._jl_callmethod($(pyjl_methodnum(pyjltype_getitem)), k)

From a3a2a91eec137738125e7db18bb2ebac9b060de7 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 10:00:30 +0100
Subject: [PATCH 02/39] remove RawValue, TypeValue and ModuleValue from
 juliacall

---
 docs/src/releasenotes.md |   7 ++
 src/C/C.jl               |   1 -
 src/JlWrap/C.jl          |   2 +
 src/JlWrap/JlWrap.jl     |   8 ---
 src/JlWrap/any.jl        |  85 ++++++++++++++++-------
 src/JlWrap/base.jl       |   4 +-
 src/JlWrap/module.jl     |  30 --------
 src/JlWrap/raw.jl        | 143 ---------------------------------------
 src/JlWrap/type.jl       |  30 --------
 src/PythonCall.jl        |   2 +-
 10 files changed, 74 insertions(+), 238 deletions(-)
 delete mode 100644 src/JlWrap/module.jl
 delete mode 100644 src/JlWrap/raw.jl
 delete mode 100644 src/JlWrap/type.jl

diff --git a/docs/src/releasenotes.md b/docs/src/releasenotes.md
index d50f76a4..2e2a14c8 100644
--- a/docs/src/releasenotes.md
+++ b/docs/src/releasenotes.md
@@ -2,6 +2,13 @@
 
 ## Unreleased (v1)
 * `PythonCall.GC` is now more like `Base.GC`: `enable(true)` replaces `enable()`, `enable(false)` replaces `disable()`, and `gc()` is added.
+* Breaking changes to Julia wrapper types:
+  * Classes renamed: `ValueBase` to `JlBase`, `AnyValue` to `Jl`, `ArrayValue` to `JlArray`, etc.
+  * Classes removed: `RawValue`, `ModuleValue`, `TypeValue`.
+  * `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.
+  * Methods renamed: `_jl_display()` to `jl_display()`, `_jl_help()` to `jl_help()`, etc.
+  * Methods removed: `_jl_raw()`
 
 ## 0.9.20 (2024-05-01)
 * The IPython extension is now automatically loaded upon import if IPython is detected.
diff --git a/src/C/C.jl b/src/C/C.jl
index 4ceda829..01bc7a4c 100644
--- a/src/C/C.jl
+++ b/src/C/C.jl
@@ -8,7 +8,6 @@ module C
 using Base: @kwdef
 using UnsafePointers: UnsafePtr
 using CondaPkg: CondaPkg
-using Pkg: Pkg
 using Requires: @require
 using Libdl: dlpath, dlopen, dlopen_e, dlclose, dlsym, dlsym_e, RTLD_LAZY, RTLD_DEEPBIND, RTLD_GLOBAL
 
diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl
index 1451a1dd..a7a23db1 100644
--- a/src/JlWrap/C.jl
+++ b/src/JlWrap/C.jl
@@ -321,6 +321,8 @@ function __init__()
     end
 end
 
+PyJuliaValue_Check(o::C.PyPtr) = C.PyObject_IsInstance(o, PyJuliaBase_Type[])
+
 PyJuliaValue_IsNull(o::C.PyPtr) = UnsafePtr{PyJuliaValueObject}(o).value[] == 0
 
 PyJuliaValue_GetValue(o::C.PyPtr) = PYJLVALUES[UnsafePtr{PyJuliaValueObject}(o).value[]]
diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index 7614b053..f9b90534 100644
--- a/src/JlWrap/JlWrap.jl
+++ b/src/JlWrap/JlWrap.jl
@@ -11,18 +11,14 @@ using ..Core: C, Utils, pynew, @autopy, incref, decref, setptr!, getptr, pyjulia
 using ..Convert: pyconvert, @pyconvert, PYCONVERT_PRIORITY_WRAP, pyconvert_add_rule, pyconvert_tryconvert, pyconvertarg, pyconvert_result
 using ..GC: GC
 
-using Pkg: Pkg
 using Base: @propagate_inbounds, allocatedinline
 
 import ..Core: Py
 
 include("C.jl")
 include("base.jl")
-include("raw.jl")
 include("any.jl")
 include("iter.jl")
-include("type.jl")
-include("module.jl")
 include("io.jl")
 include("number.jl")
 include("objectarray.jl")
@@ -35,11 +31,8 @@ include("callback.jl")
 function __init__()
     Cjl.C.with_gil() do 
         init_base()
-        init_raw()
         init_any()
         init_iter()
-        init_type()
-        init_module()
         init_io()
         init_number()
         init_array()
@@ -52,7 +45,6 @@ function __init__()
         jl.Core = Base.Core
         jl.Base = Base
         jl.Main = Main
-        jl.Pkg = Pkg
         jl.PythonCall = PythonCall
     end
 end
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index cddec22c..3ec88f86 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -1,20 +1,26 @@
 const pyjlanytype = pynew()
 
+pyjlany(x) = pyjl(pyjlanytype, x)
+
 # pyjlany_repr(self) = Py("<jl $(repr(self))>")
 function pyjlany_repr(self)
     str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit=>true, :displaysize=>(23,80)))
     # type = self isa Function ? "Function" : self isa Type ? "Type" : nameof(typeof(self))
     sep = '\n' in str ? '\n' : ' '
-    Py("Julia:$sep$str")
+    Py("Julia:$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))
+    pyjlany(getproperty(self, k))
 end
 pyjl_handle_error_type(::typeof(pyjlany_getattr), self, exc) = pybuiltins.AttributeError
 
@@ -27,18 +33,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!(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 = pyjlany(self(args...; kwargs...))
     elseif pylen(args_) > 0
         args = pyconvert(Vector{Any}, args_)
-        ans = Py(self(args...))
+        ans = pyjlany(self(args...))
     else
-        ans = Py(self())
+        ans = pyjlany(self())
     end
     pydel!(args_)
     pydel!(kwargs_)
@@ -47,13 +64,24 @@ end
 pyjl_handle_error_type(::typeof(pyjlany_call), self, exc) = exc isa 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_)
+            pyjlany(self{k...})
+        else
+            k = pyconvert(Any, k_)
+            pyjlany(self{k})
+        end    
     else
-        k = pyconvert(Any, k_)
-        Py(self[k])
+        if pyistuple(k_)
+            k = pyconvert(Vector{Any}, k_)
+            pydel!(k_)
+            pyjlany(self[k...])
+        else
+            k = pyconvert(Any, k_)
+            pyjlany(self[k])
+        end
     end
 end
 pyjl_handle_error_type(::typeof(pyjlany_getitem), self, exc) = exc isa BoundsError ? pybuiltins.IndexError : exc isa KeyError ? pybuiltins.KeyError : PyNULL
@@ -85,7 +113,7 @@ function pyjlany_delitem(self, k_::Py)
 end
 pyjl_handle_error_type(::typeof(pyjlany_delitem), self, exc) = exc isa BoundsError ? pybuiltins.IndexError : exc isa KeyError ? pybuiltins.KeyError : PyNULL
 
-pyjlany_contains(self, v::Py) = Py(@pyconvert(eltype(self), v, return Py(false)) in self)
+pyjlany_contains(self, v::Py) = Py((@pyconvert(eltype(self), v, return Py(false)) in self)::Bool)
 pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = exc isa MethodError && exc.f === in ? pybuiltins.TypeError : PyNULL
 
 struct pyjlany_op{OP}
@@ -96,7 +124,7 @@ function (op::pyjlany_op)(self, other_::Py)
     if pyisjl(other_)
         other = pyjlvalue(other_)
         pydel!(other_)
-        Py(op.op(self, other))
+        pyjlany(op.op(self, other))
     else
         pybuiltins.NotImplemented
     end
@@ -107,7 +135,7 @@ function (op::pyjlany_op)(self, other_::Py, other2_::Py)
         other2 = pyjlvalue(other2_)
         pydel!(other_)
         pydel!(other2_)
-        Py(op.op(self, other, other2))
+        pyjlany(op.op(self, other, other2))
     else
         pybuiltins.NotImplemented
     end
@@ -121,7 +149,7 @@ function (op::pyjlany_rev_op)(self, other_::Py)
     if pyisjl(other_)
         other = pyjlvalue(other_)
         pydel!(other_)
-        Py(op.op(other, self))
+        pyjlany(op.op(other, self))
     else
         pybuiltins.NotImplemented
     end
@@ -132,14 +160,14 @@ function (op::pyjlany_rev_op)(self, other_::Py, other2_::Py)
         other2 = pyjlvalue(other2_)
         pydel!(other_)
         pydel!(other2_)
-        Py(op.op(other, self, other2))
+        pyjlany(op.op(other, self, other2))
     else
         pybuiltins.NotImplemented
     end
 end
 pyjl_handle_error_type(op::pyjlany_rev_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL
 
-pyjlany_name(self) = Py(string(nameof(self)))
+pyjlany_name(self) = Py(string(nameof(self))::String)
 pyjl_handle_error_type(::typeof(pyjlany_name), self, exc) = exc isa MethodError && exc.f === nameof ? pybuiltins.AttributeError : PyNULL
 
 function pyjlany_display(self, mime_::Py)
@@ -187,6 +215,17 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py)
     return ans
 end
 
+function pyjlany_eval(self, expr::Py)
+    if self isa Module
+        Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
+    else
+        throw(ArgumentError("self must be a Julia 'Module', got a '$(typeof(self))'"))
+    end
+end
+
+pyjl_handle_error_type(::typeof(pyjlany_eval), self, exc) = exc isa ArgumentError ? pybuiltins.TypeError : PyNULL
+
+
 function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
@@ -316,12 +355,12 @@ function init_any()
         @property
         def __name__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_name)))
-        def _jl_raw(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlraw)))
-        def _jl_display(self, mime=None):
+        def jl_display(self, mime=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_display)), mime)
-        def _jl_help(self, mime=None):
+        def jl_help(self, mime=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_help)), mime)
+        def jl_eval(self, expr):
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_eval)), expr)
         def _repr_mimebundle_(self, include=None, exclude=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude)
     """, @__FILE__(), "exec"), jl.__dict__)
diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl
index 6d97412c..0e6e8e46 100644
--- a/src/JlWrap/base.jl
+++ b/src/JlWrap/base.jl
@@ -91,7 +91,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((pyjlany(exc), pyjlany(catch_backtrace()))))
                     return C.PyNULL
                 end
             catch
@@ -107,7 +107,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((pyjlany(exc), pyjlany(catch_backtrace()))))
         return C.PyNULL
     elseif pyistype(t)
         # Exception type => raise this type of error
diff --git a/src/JlWrap/module.jl b/src/JlWrap/module.jl
deleted file mode 100644
index 02257658..00000000
--- a/src/JlWrap/module.jl
+++ /dev/null
@@ -1,30 +0,0 @@
-const pyjlmoduletype = pynew()
-
-function pyjlmodule_dir(self::Module)
-    ks = Symbol[]
-    append!(ks, names(self, all = true, imported = true))
-    for m in ccall(:jl_module_usings, Any, (Any,), self)::Vector
-        append!(ks, names(m))
-    end
-    pylist(pyjl_attr_jl2py(string(k)) for k in ks)
-end
-
-function pyjlmodule_seval(self::Module, expr::Py)
-    Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
-end
-
-function init_module()
-    jl = pyjuliacallmodule
-    pybuiltins.exec(pybuiltins.compile("""
-    $("\n"^(@__LINE__()-1))
-    class ModuleValue(JlBase):
-        __slots__ = ()
-        def __dir__(self):
-            return JlBase.__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/raw.jl b/src/JlWrap/raw.jl
deleted file mode 100644
index 1fd56330..00000000
--- a/src/JlWrap/raw.jl
+++ /dev/null
@@ -1,143 +0,0 @@
-const pyjlrawtype = pynew()
-
-pyjlraw_repr(self) = Py("<jl $(repr(self))>")
-
-pyjlraw_str(self) = Py(sprint(print, self))
-
-pyjl_attr_py2jl(k::String) = replace(k, r"_[b]+$" => (x -> "!"^(length(x) - 1)))
-
-pyjl_attr_jl2py(k::String) = replace(k, r"!+$" => (x -> "_" * "b"^length(x)))
-
-function pyjlraw_getattr(self, k_::Py)
-    k = Symbol(pyjl_attr_py2jl(pyconvert(String, k_)))
-    pydel!(k_)
-    pyjlraw(getproperty(self, k))
-end
-
-function pyjlraw_setattr(self, k_::Py, v_::Py)
-    k = Symbol(pyjl_attr_py2jl(pyconvert(String, k_)))
-    pydel!(k_)
-    v = pyconvert(Any, v_)
-    setproperty!(self, k, v)
-    Py(nothing)
-end
-
-pyjlraw_dir(self) = pylist(pyjl_attr_jl2py(string(k)) for k in propertynames(self, true))
-
-function pyjlraw_call(self, args_::Py, kwargs_::Py)
-    if pylen(kwargs_) > 0
-        args = pyconvert(Vector{Any}, args_)
-        kwargs = pyconvert(Dict{Symbol,Any}, kwargs_)
-        ans = pyjlraw(self(args...; kwargs...))
-    elseif pylen(args_) > 0
-        args = pyconvert(Vector{Any}, args_)
-        ans = pyjlraw(self(args...))
-    else
-        ans = pyjlraw(self())
-    end
-    pydel!(args_)
-    pydel!(kwargs_)
-    ans
-end
-
-pyjlraw_len(self) = Py(length(self))
-
-function pyjlraw_getitem(self, k_::Py)
-    if pyistuple(k_)
-        k = pyconvert(Vector{Any}, k_)
-        pydel!(k_)
-        pyjlraw(self[k...])
-    else
-        k = pyconvert(Any, k_)
-        pyjlraw(self[k])
-    end
-end
-
-function pyjlraw_setitem(self, k_::Py, v_::Py)
-    v = pyconvert(Any, v_)
-    if pyistuple(k_)
-        k = pyconvert(Vector{Any}, k_)
-        pydel!(k_)
-        self[k...] = v
-    else
-        k = pyconvert(Any, k_)
-        self[k] = v
-    end
-    Py(nothing)
-end
-
-function pyjlraw_delitem(self, k_::Py)
-    if pyistuple(k_)
-        k = pyconvert(Vector{Any}, k_)
-        pydel!(k_)
-        delete!(self, k...)
-    else
-        k = pyconvert(Any, k_)
-        delete!(self, k)
-    end
-    Py(nothing)
-end
-
-pyjlraw_bool(self::Bool) = Py(self)
-pyjlraw_bool(self) = (errset(pybuiltins.TypeError, "Only Julia 'Bool' can be tested for truthyness"); PyNULL)
-
-function init_raw()
-    jl = pyjuliacallmodule
-    pybuiltins.exec(pybuiltins.compile("""
-    $("\n"^(@__LINE__()-1))
-    class RawValue(JlBase):
-        __slots__ = ()
-        def __repr__(self):
-            if self._jl_isnull():
-                return "<jl NULL>"
-            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:
-                JlBase.__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 JlBase.__dir__(self) + self._jl_callmethod($(pyjl_methodnum(pyjlraw_dir)))
-        def __call__(self, *args, **kwargs):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlraw_call)), args, kwargs)
-        def __len__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlraw_len)))
-        def __getitem__(self, k):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlraw_getitem)), k)
-        def __setitem__(self, k, v):
-            self._jl_callmethod($(pyjl_methodnum(pyjlraw_setitem)), k, v)
-        def __delitem__(self, k):
-            self._jl_callmethod($(pyjl_methodnum(pyjlraw_delitem)), k)
-        def __bool__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlraw_bool)))
-        def _jl_any(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjl)))
-    """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjlrawtype, jl.RawValue)
-end
-
-"""
-    pyjlraw(v)
-
-Create a Python object wrapping the Julia object `x`.
-
-It has type `juliacall.RawValue`. This has a much more rigid "Julian" interface than `pyjl(v)`.
-For example, accessing attributes or calling this object will always return a `RawValue`.
-"""
-pyjlraw(v) = pyjl(pyjlrawtype, v)
-export pyjlraw
diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl
deleted file mode 100644
index 050605e4..00000000
--- a/src/JlWrap/type.jl
+++ /dev/null
@@ -1,30 +0,0 @@
-const pyjltypetype = pynew()
-
-function pyjltype_getitem(self::Type, k_)
-    if pyistuple(k_)
-        k = pyconvert(Vector{Any}, k_)
-        pydel!(k_)
-        Py(self{k...})
-    else
-        k = pyconvert(Any, k_)
-        Py(self{k})
-    end
-end
-
-function init_type()
-    jl = pyjuliacallmodule
-    pybuiltins.exec(pybuiltins.compile("""
-    $("\n"^(@__LINE__()-1))
-    class TypeValue(JlBase):
-        __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/PythonCall.jl b/src/PythonCall.jl
index fdd1774f..36a605f4 100644
--- a/src/PythonCall.jl
+++ b/src/PythonCall.jl
@@ -38,7 +38,7 @@ for k in [:event_loop_on, :event_loop_off, :fix_qt_plugin_path]
 end
 
 # not API but used in tests
-for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlmoduletype, :pyjlintegertype, :pyjlrationaltype, :pyjlrealtype, :pyjlcomplextype, :pyjlsettype, :pyjltypetype]
+for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlintegertype, :pyjlrationaltype, :pyjlrealtype, :pyjlcomplextype, :pyjlsettype]
     @eval using .JlWrap: $k
 end
 

From 35e24244a86122b01994ac8c8f70c8cb87c86774 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 16:44:18 +0100
Subject: [PATCH 03/39] add JlBase.__init__ and store nothing/true/false
 specially

---
 src/JlWrap/C.jl        | 116 +++++++++++++++++++++++++++++++++--------
 src/JlWrap/any.jl      |  10 +---
 src/JlWrap/base.jl     |  20 +------
 src/JlWrap/callback.jl |  10 +---
 4 files changed, 98 insertions(+), 58 deletions(-)

diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl
index a7a23db1..a4ac547b 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
@@ -30,7 +32,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 +41,59 @@ 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
+        C.PyErr_SetString(C.POINTERS.PyExc_Exception, "error during __init__()")
+        @debug "exception" exc
+        Cint(-1)
+    end
+end
+
 const PYJLMETHODS = Vector{Any}()
 
 function PyJulia_MethodNum(f)
@@ -47,12 +102,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
@@ -253,7 +302,6 @@ end
 
 const _pyjlbase_name = "juliacall.JlBase"
 const _pyjlbase_type = fill(C.PyTypeObject())
-const _pyjlbase_isnull_name = "_jl_isnull"
 const _pyjlbase_callmethod_name = "_jl_callmethod"
 const _pyjlbase_reduce_name = "__reduce__"
 const _pyjlbase_serialize_name = "_jl_serialize"
@@ -269,11 +317,6 @@ function init_c()
             meth = @cfunction(_pyjl_callmethod, C.PyPtr, (C.PyPtr, C.PyPtr)),
             flags = C.Py_METH_VARARGS,
         ),
-        C.PyMethodDef(
-            name = pointer(_pyjlbase_isnull_name),
-            meth = @cfunction(_pyjl_isnull, C.PyPtr, (C.PyPtr, C.PyPtr)),
-            flags = C.Py_METH_NOARGS,
-        ),
         C.PyMethodDef(
             name = pointer(_pyjlbase_reduce_name),
             meth = @cfunction(_pyjl_reduce, C.PyPtr, (C.PyPtr, C.PyPtr)),
@@ -298,13 +341,11 @@ function init_c()
     _pyjlbase_type[] = C.PyTypeObject(
         name = pointer(_pyjlbase_name),
         basicsize = sizeof(PyJuliaValueObject),
-        # new = C.POINTERS.PyType_GenericNew,
         new = @cfunction(_pyjl_new, C.PyPtr, (C.PyPtr, C.PyPtr, C.PyPtr)),
         dealloc = @cfunction(_pyjl_dealloc, Cvoid, (C.PyPtr,)),
+        init = @cfunction(_pyjl_init, Cint, (C.PyPtr, C.PyPtr, C.PyPtr)),
         flags = C.Py_TPFLAGS_BASETYPE | C.Py_TPFLAGS_HAVE_VERSION_TAG,
         weaklistoffset = fieldoffset(PyJuliaValueObject, 3),
-        # getattro = C.POINTERS.PyObject_GenericGetAttr,
-        # setattro = C.POINTERS.PyObject_GenericSetAttr,
         methods = pointer(_pyjlbase_methods),
         as_buffer = pointer(_pyjlbase_as_buffer),
     )
@@ -323,13 +364,44 @@ end
 
 PyJuliaValue_Check(o::C.PyPtr) = C.PyObject_IsInstance(o, PyJuliaBase_Type[])
 
-PyJuliaValue_IsNull(o::C.PyPtr) = UnsafePtr{PyJuliaValueObject}(o).value[] == 0
+function PyJuliaValue_GetValue(o::C.PyPtr)
+    v = UnsafePtr{PyJuliaValueObject}(o).value[]
+    if v == 0
+        nothing
+    elseif v > 0
+        PYJLVALUES[v]
+    elseif v == -1
+        false
+    elseif v == -2
+        true
+    end
+end
+
+function PyJuliaValue_SetValue(o::C.PyPtr, v::Union{Nothing,Bool})
+    idx = UnsafePtr{PyJuliaValueObject}(o).value[]
+    if idx >= 1
+        PYJLVALUES[idx] = nothing
+        push!(PYJLFREEVALUES, idx)
+    end
+    if v === nothing
+        idx = 0
+    elseif v === false
+        idx = -1
+    elseif v === true
+        idx = -2
+    else
+        @assert false
+    end
+    UnsafePtr{PyJuliaValueObject}(o).value[] = idx
+    nothing
+end
 
-PyJuliaValue_GetValue(o::C.PyPtr) = PYJLVALUES[UnsafePtr{PyJuliaValueObject}(o).value[]]
 
-PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v)) = begin
+function PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v))
     idx = UnsafePtr{PyJuliaValueObject}(o).value[]
-    if idx == 0
+    if idx >= 1
+        PYJLVALUES[idx] = v
+    else
         if isempty(PYJLFREEVALUES)
             push!(PYJLVALUES, v)
             idx = length(PYJLVALUES)
@@ -338,13 +410,11 @@ PyJuliaValue_SetValue(o::C.PyPtr, @nospecialize(v)) = begin
             PYJLVALUES[idx] = v
         end
         UnsafePtr{PyJuliaValueObject}(o).value[] = idx
-    else
-        PYJLVALUES[idx] = v
     end
     nothing
 end
 
-PyJuliaValue_New(t::C.PyPtr, @nospecialize(v)) = begin
+function PyJuliaValue_New(t::C.PyPtr, @nospecialize(v))
     if C.PyType_IsSubtype(t, PyJuliaBase_Type[]) != 1
         C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.JlBase'")
         return C.PyNULL
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 3ec88f86..91c33418 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -233,15 +233,9 @@ function init_any()
     class Jl(JlBase):
         __slots__ = ()
         def __repr__(self):
-            if self._jl_isnull():
-                return "<jl NULL>"
-            else:
-                return self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
         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)
diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl
index 0e6e8e46..63caf20f 100644
--- a/src/JlWrap/base.jl
+++ b/src/JlWrap/base.jl
@@ -14,26 +14,12 @@ Test whether `x` is a wrapped Julia value, namely an instance of `juliacall.JlBa
 pyisjl(x) = pytypecheck(x, pyjlbasetype)
 export pyisjl
 
-pyjlisnull(x) = @autopy x begin
-    if pyisjl(x_)
-        Cjl.PyJuliaValue_IsNull(getptr(x_))
-    else
-        error("Expecting a 'juliacall.JlBase', got a '$(pytype(x_).__name__)'")
-    end
-end
-
 """
     pyjlvalue(x)
 
 Extract the value from the wrapped Julia value `x`.
 """
-pyjlvalue(x) = @autopy x begin
-    if pyjlisnull(x_)
-        error("Julia value is NULL")
-    else
-        _pyjl_getvalue(x_)
-    end
-end
+pyjlvalue(x) = @autopy x _pyjl_getvalue(x_)
 export pyjlvalue
 
 function init_base()
@@ -49,10 +35,6 @@ pyconvert_rule_jlvalue(::Type{T}, x::Py) where {T} = pyconvert_tryconvert(T, _py
 
 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
diff --git a/src/JlWrap/callback.jl b/src/JlWrap/callback.jl
index 1c82b455..f018cc31 100644
--- a/src/JlWrap/callback.jl
+++ b/src/JlWrap/callback.jl
@@ -42,15 +42,9 @@ function init_callback()
     class CallbackValue(JlBase):
         __slots__ = ()
         def __repr__(self):
-            if self._jl_isnull():
-                return "<jl NULL>"
-            else:
-                return self._jl_callmethod($(pyjl_methodnum(pyjlcallback_repr)))
+            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)))
+            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__)

From a3b7d5b85ff3428cd02b1321eeba4c6a39bbede2 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 17:20:41 +0100
Subject: [PATCH 04/39] more general numeric operators

---
 src/JlWrap/any.jl | 80 ++++++++++++++++++++++++++++++++++-------------
 1 file changed, 58 insertions(+), 22 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 91c33418..e4db6cbe 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -119,26 +119,28 @@ pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = exc isa MethodEr
 struct pyjlany_op{OP}
     op :: OP
 end
-(op::pyjlany_op)(self) = Py(op.op(self))
+(op::pyjlany_op)(self) = pyjlany(op.op(self))
 function (op::pyjlany_op)(self, other_::Py)
     if pyisjl(other_)
         other = pyjlvalue(other_)
         pydel!(other_)
-        pyjlany(op.op(self, other))
     else
-        pybuiltins.NotImplemented
+        other = pyconvert(Any, other_)
     end
+    pyjlany(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_)
-        pyjlany(op.op(self, other, other2))
     else
-        pybuiltins.NotImplemented
+        other = pyconvert(Any, other)
     end
+    if pyisjl(other2_)
+        other2 = pyjlvalue(other2_)
+        pydel!(other2_)
+    end
+    pyjlany(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
 
@@ -149,21 +151,23 @@ function (op::pyjlany_rev_op)(self, other_::Py)
     if pyisjl(other_)
         other = pyjlvalue(other_)
         pydel!(other_)
-        pyjlany(op.op(other, self))
     else
-        pybuiltins.NotImplemented
+        other = pyconvert(Any, other_)
     end
+    pyjlany(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_)
-        pyjlany(op.op(other, self, other2))
     else
-        pybuiltins.NotImplemented
+        other = pyconvert(Any, other)
+    end
+    if pyisjl(other2_)
+        other2 = pyjlvalue(other2_)
+        pydel!(other2_)
     end
+    pyjlany(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
 
@@ -215,16 +219,38 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py)
     return ans
 end
 
-function pyjlany_eval(self, expr::Py)
-    if self isa Module
-        Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
+pyjlany_eval(self::Module, expr::Py) = Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
+
+pyjl_handle_error_type(::typeof(pyjlany_eval), self, exc) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+
+pyjlany_int(self) = pyint(convert(Integer, self))
+
+pyjl_handle_error_type(::typeof(pyjlany_int), self, exc) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+
+pyjlany_float(self) = pyfloat(convert(AbstractFloat, self))
+
+pyjl_handle_error_type(::typeof(pyjlany_float), self, exc) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+
+pyjlany_complex(self) = pycomplex(convert(Complex, self))
+
+pyjl_handle_error_type(::typeof(pyjlany_complex), self, exc) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+
+function pyjlany_index(self)
+    if self isa Integer
+        pyint(self)
     else
-        throw(ArgumentError("self must be a Julia 'Module', got a '$(typeof(self))'"))
+        errset(pybuiltins.TypeError, "Only Julia 'Integer' values can be used as Python indices, not '$(typeof(self))'")
     end
 end
 
-pyjl_handle_error_type(::typeof(pyjlany_eval), self, exc) = exc isa ArgumentError ? pybuiltins.TypeError : PyNULL
-
+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
 
 function init_any()
     jl = pyjuliacallmodule
@@ -255,7 +281,7 @@ function init_any()
         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):
@@ -276,6 +302,8 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(-))))
         def __abs__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(abs))))
+        def abs(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(abs))))
         def __invert__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(~))))
         def __add__(self, other):
@@ -346,6 +374,14 @@ function init_any()
             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)))
         @property
         def __name__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_name)))

From 7691d9972d78f71a358198425aafa95c1779af2a Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 17:46:55 +0100
Subject: [PATCH 05/39] rounding methods, jl_to_py, remove jlwrap numbers

---
 src/JlWrap/JlWrap.jl |   2 -
 src/JlWrap/any.jl    |  43 ++++++--
 src/JlWrap/number.jl | 233 -------------------------------------------
 3 files changed, 35 insertions(+), 243 deletions(-)
 delete mode 100644 src/JlWrap/number.jl

diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index f9b90534..6701e27a 100644
--- a/src/JlWrap/JlWrap.jl
+++ b/src/JlWrap/JlWrap.jl
@@ -20,7 +20,6 @@ include("base.jl")
 include("any.jl")
 include("iter.jl")
 include("io.jl")
-include("number.jl")
 include("objectarray.jl")
 include("array.jl")
 include("vector.jl")
@@ -34,7 +33,6 @@ function __init__()
         init_any()
         init_iter()
         init_io()
-        init_number()
         init_array()
         init_vector()
         init_dict()
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index e4db6cbe..b3a9fa42 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -220,26 +220,23 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py)
 end
 
 pyjlany_eval(self::Module, expr::Py) = Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
-
-pyjl_handle_error_type(::typeof(pyjlany_eval), self, exc) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+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) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+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) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+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) = exc isa MethodError ? pybuiltins.TypeError : PyNULL
+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
 
@@ -252,6 +249,23 @@ function pyjlany_bool(self)
     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_)
+    pyjlany(round(self; digits = ndigits))
+end
+pyjl_handle_error_type(::typeof(pyjlany_round), self, exc::MethodError) = pybuiltins.TypeError
+
 function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
@@ -382,6 +396,17 @@ function init_any()
             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)))
@@ -391,6 +416,8 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_help)), mime)
         def jl_eval(self, expr):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_eval)), expr)
+        def jl_to_py(self):
+            return self._jl_callmethod($(pyjl_methodnum(Py)))
         def _repr_mimebundle_(self, include=None, exclude=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude)
     """, @__FILE__(), "exec"), jl.__dict__)
diff --git a/src/JlWrap/number.jl b/src/JlWrap/number.jl
deleted file mode 100644
index 0f43c64a..00000000
--- a/src/JlWrap/number.jl
+++ /dev/null
@@ -1,233 +0,0 @@
-const pyjlnumbertype = pynew()
-const pyjlcomplextype = pynew()
-const pyjlrealtype = pynew()
-const pyjlrationaltype = pynew()
-const pyjlintegertype = pynew()
-
-struct pyjlnumber_op{OP}
-    op :: OP
-end
-(op::pyjlnumber_op)(self) = Py(op.op(self))
-function (op::pyjlnumber_op)(self, other_::Py)
-    if pyisjl(other_)
-        other = pyjlvalue(other_)
-        pydel!(other_)
-    else
-        other = @pyconvert(Number, other_, return pybuiltins.NotImplemented)
-    end
-    Py(op.op(self, other))
-end
-function (op::pyjlnumber_op)(self, other_::Py, other2_::Py)
-    if pyisjl(other_)
-        other = pyjlvalue(other_)
-        pydel!(other_)
-    else
-        other = @pyconvert(Number, other_, return pybuiltins.NotImplemented)
-    end
-    if pyisjl(other2_)
-        other2 = pyjlvalue(other2_)
-        pydel!(other2_)
-    else
-        other2 = @pyconvert(Number, other2_, return pybuiltins.NotImplemented)
-    end
-    Py(op.op(self, other, other2))
-end
-pyjl_handle_error_type(op::pyjlnumber_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL
-
-struct pyjlnumber_rev_op{OP}
-    op :: OP
-end
-function (op::pyjlnumber_rev_op)(self, other_::Py)
-    if pyisjl(other_)
-        other = pyjlvalue(other_)
-        pydel!(other_)
-    else
-        other = @pyconvert(Number, other_, return pybuiltins.NotImplemented)
-    end
-    Py(op.op(other, self))
-end
-function (op::pyjlnumber_rev_op)(self, other_::Py, other2_::Py)
-    if pyisjl(other_)
-        other = pyjlvalue(other_)
-        pydel!(other_)
-    else
-        other = @pyconvert(Number, other_, return pybuiltins.NotImplemented)
-    end
-    if pyisjl(other2_)
-        other2 = pyjlvalue(other2_)
-        pydel!(other2_)
-    else
-        other2 = @pyconvert(Number, other2_, return pybuiltins.NotImplemented)
-    end
-    Py(op.op(other, self, other2))
-end
-pyjl_handle_error_type(op::pyjlnumber_rev_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL
-
-pyjlreal_trunc(self::Real) = Py(trunc(Integer, self))
-pyjl_handle_error_type(::typeof(pyjlreal_trunc), self, exc::MethodError) = exc.f === trunc ? pybuiltins.TypeError : PyNULL
-
-pyjlreal_floor(self::Real) = Py(floor(Integer, self))
-pyjl_handle_error_type(::typeof(pyjlreal_floor), self, exc::MethodError) = exc.f === floor ? pybuiltins.TypeError : PyNULL
-
-pyjlreal_ceil(self::Real) = Py(ceil(Integer, self))
-pyjl_handle_error_type(::typeof(pyjlreal_ceil), self, exc::MethodError) = exc.f === ceil ? pybuiltins.TypeError : PyNULL
-
-function pyjlreal_round(self::Real, ndigits_::Py)
-    ndigits = pyconvertarg(Union{Int,Nothing}, ndigits_, "ndigits")
-    pydel!(ndigits_)
-    if ndigits === nothing
-        Py(round(Integer, self))
-    else
-        Py(round(self; digits = ndigits))
-    end
-end
-pyjl_handle_error_type(::typeof(pyjlreal_round), self, exc::MethodError) = exc.f === round ? pybuiltins.TypeError : PyNULL
-
-function init_number()
-    jl = pyjuliacallmodule
-    pybuiltins.exec(pybuiltins.compile("""
-    $("\n"^(@__LINE__()-1))
-    class NumberValue(JlBase):
-        __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

From 6b6d3dee74e20354b39fedbd070d366d85cb8d94 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 17:48:20 +0100
Subject: [PATCH 06/39] update release notes

---
 docs/src/releasenotes.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/src/releasenotes.md b/docs/src/releasenotes.md
index 2e2a14c8..77158e67 100644
--- a/docs/src/releasenotes.md
+++ b/docs/src/releasenotes.md
@@ -4,7 +4,7 @@
 * `PythonCall.GC` is now more like `Base.GC`: `enable(true)` replaces `enable()`, `enable(false)` replaces `disable()`, and `gc()` is added.
 * Breaking changes to Julia wrapper types:
   * Classes renamed: `ValueBase` to `JlBase`, `AnyValue` to `Jl`, `ArrayValue` to `JlArray`, etc.
-  * Classes removed: `RawValue`, `ModuleValue`, `TypeValue`.
+  * 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.
   * Methods renamed: `_jl_display()` to `jl_display()`, `_jl_help()` to `jl_help()`, etc.

From bfc84e7961e2a1510780c79112fe23414bb95014 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 17:56:31 +0100
Subject: [PATCH 07/39] remove references to nonexistent types

---
 src/PythonCall.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/PythonCall.jl b/src/PythonCall.jl
index 36a605f4..f904b63b 100644
--- a/src/PythonCall.jl
+++ b/src/PythonCall.jl
@@ -38,7 +38,7 @@ for k in [:event_loop_on, :event_loop_off, :fix_qt_plugin_path]
 end
 
 # not API but used in tests
-for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlintegertype, :pyjlrationaltype, :pyjlrealtype, :pyjlcomplextype, :pyjlsettype]
+for k in [:pyjlanytype, :pyjlarraytype, :pyjlvectortype, :pyjlbinaryiotype, :pyjltextiotype, :pyjldicttype, :pyjlsettype]
     @eval using .JlWrap: $k
 end
 

From 6b47a0eb79443baa13ab4a706d76ab6c983d94a9 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 17:56:41 +0100
Subject: [PATCH 08/39] add jl_callback

---
 src/JlWrap/any.jl | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index b3a9fa42..c6859ce9 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -63,6 +63,23 @@ function pyjlany_call(self, args_::Py, kwargs_::Py)
 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_getitem(self, k_::Py)
     if self isa Type
         if pyistuple(k_)
@@ -418,6 +435,8 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_eval)), expr)
         def jl_to_py(self):
             return self._jl_callmethod($(pyjl_methodnum(Py)))
+        def jl_callback(self, *args, **kwargs):
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_callback)), args, kwargs)
         def _repr_mimebundle_(self, include=None, exclude=None):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude)
     """, @__FILE__(), "exec"), jl.__dict__)

From f34317d44c99af31d61e728261d0a8dae32a4e13 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sat, 4 May 2024 18:08:13 +0100
Subject: [PATCH 09/39] simpler iteration

---
 src/JlWrap/JlWrap.jl |  2 --
 src/JlWrap/any.jl    | 44 +++++++++++++++++++++++++++++++++++++++++++-
 src/JlWrap/iter.jl   | 42 ------------------------------------------
 3 files changed, 43 insertions(+), 45 deletions(-)
 delete mode 100644 src/JlWrap/iter.jl

diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index 6701e27a..ee814967 100644
--- a/src/JlWrap/JlWrap.jl
+++ b/src/JlWrap/JlWrap.jl
@@ -18,7 +18,6 @@ import ..Core: Py
 include("C.jl")
 include("base.jl")
 include("any.jl")
-include("iter.jl")
 include("io.jl")
 include("objectarray.jl")
 include("array.jl")
@@ -31,7 +30,6 @@ function __init__()
     Cjl.C.with_gil() do 
         init_base()
         init_any()
-        init_iter()
         init_io()
         init_array()
         init_vector()
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index c6859ce9..9aeebe9e 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -283,6 +283,46 @@ function pyjlany_round(self, ndigits_::Py)
 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
+        pyjlany(s[1])
+    end
+end
+
+pyjlany_hash(self) = pyint(hash(self))
+
 function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
@@ -323,6 +363,8 @@ function init_any()
             self._jl_callmethod($(pyjl_methodnum(pyjlany_delitem)), k)
         def __iter__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(Iterator))))
+        def __next__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_next)))
         def __reversed__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(reverse))))
         def __contains__(self, v):
@@ -404,7 +446,7 @@ function init_any()
         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))))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash)))
         def __int__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_int)))
         def __float__(self):
diff --git a/src/JlWrap/iter.jl b/src/JlWrap/iter.jl
deleted file mode 100644
index 499afcf7..00000000
--- a/src/JlWrap/iter.jl
+++ /dev/null
@@ -1,42 +0,0 @@
-mutable struct Iterator
-    val::Any
-    st::Any
-end
-Iterator(x) = Iterator(x, nothing)
-Base.length(x::Iterator) = length(x.val)
-
-const pyjlitertype = pynew()
-
-function pyjliter_next(self::Iterator)
-    val = self.val
-    st = self.st
-    if st === nothing
-        z = iterate(val)
-    else
-        z = iterate(val, something(st))
-    end
-    if z === nothing
-        errset(pybuiltins.StopIteration)
-        PyNULL
-    else
-        r, newst = z
-        self.st = Some(newst)
-        Py(r)
-    end
-end
-
-function init_iter()
-    jl = pyjuliacallmodule
-    pybuiltins.exec(pybuiltins.compile("""
-    $("\n"^(@__LINE__()-1))
-    class IteratorValue(JlBase):
-        __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

From a96820f200f45a6db6e94b07ec50c2bb736d5952 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 5 May 2024 09:17:16 +0100
Subject: [PATCH 10/39] simplify callbacks (pyfunc etc)

---
 src/JlWrap/JlWrap.jl   |  1 -
 src/JlWrap/callback.jl | 82 ++++++++++--------------------------------
 2 files changed, 19 insertions(+), 64 deletions(-)

diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index ee814967..d79885ba 100644
--- a/src/JlWrap/JlWrap.jl
+++ b/src/JlWrap/JlWrap.jl
@@ -35,7 +35,6 @@ function __init__()
         init_vector()
         init_dict()
         init_set()
-        init_callback()
         # add packages to juliacall
         jl = pyjuliacallmodule
         jl.Core = Base.Core
diff --git a/src/JlWrap/callback.jl b/src/JlWrap/callback.jl
index f018cc31..2070a57c 100644
--- a/src/JlWrap/callback.jl
+++ b/src/JlWrap/callback.jl
@@ -1,58 +1,6 @@
 const pywrapcallback = pynew()
-const pyjlcallbacktype = pynew()
 
-pyjlcallback_repr(self) = Py("<jl $(repr(self))>")
-
-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(JlBase):
-        __slots__ = ()
-        def __repr__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlcallback_repr)))
-        def __str__(self):
-            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) = pyjlany(f).jl_callback
 
 """
     pyfunc(f; [name], [qualname], [doc], [signature])
@@ -60,25 +8,33 @@ pyjlcallback(f) = pyjl(pyjlcallbacktype, f)
 Wrap the callable `f` as an ordinary Python function.
 
 The name, qualname, docstring or signature can optionally be set with `name`, `qualname`,
-`doc` or `signature`.
+`doc` or `signature`. If either of `name` or `qualname` are given, the other is inferred.
 
 Unlike `Py(f)` (or `pyjl(f)`), the arguments passed to `f` are always of type `Py`, i.e.
 they are never converted.
 """
-function pyfunc(f; name=nothing, qualname=name, doc=nothing, signature=nothing, wrap=pywrapcallback)
+function pyfunc(f; name=nothing, qualname=nothing, doc=nothing, signature=nothing, wrap=pywrapcallback)
     f2 = ispy(f) ? f : pyjlcallback(f)
     if wrap isa Pair
         wrapargs, wrapfunc = wrap
     else
         wrapargs, wrapfunc = (), wrap
     end
+    if wrapfunc === pywrapcallback && pyisnull(pywrapcallback)
+        pycopy!(pywrapcallback, pybuiltins.eval("lambda f: lambda *args, **kwargs: f(*args, **kwargs)", pydict()))
+    end
     if wrapfunc isa AbstractString
         f3 = pybuiltins.eval(wrapfunc, pydict())(f2, wrapargs...)
     else
         f3 = wrapfunc(f2, wrapargs...)
     end
+    if name === nothing && qualname !== nothing
+        name = split(qualname, '.')[end]
+    elseif name !== nothing && qualname === nothing
+        qualname = name
+    end
     f3.__name__ = name === nothing ? "<lambda>" : name
-    f3.__qualname__ = name === nothing ? "<lambda>" : qualname
+    f3.__qualname__ = qualname === nothing ? "<lambda>" : qualname
     if doc !== nothing
         f3.__doc__ = doc
     end
@@ -114,8 +70,8 @@ pystaticmethod(f; kw...) = pybuiltins.staticmethod(ispy(f) ? f : pyfunc(f; kw...
 export pystaticmethod
 
 """
-    pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing)
-    pyproperty(get)
+    pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing, ...)
+    pyproperty(get, set=nothing, del=nothing; doc=nothing, ...)
 
 Create a Python `property` with the given getter, setter and deleter.
 
@@ -123,12 +79,12 @@ If `get`, `set` or `del` is not a Python object (e.g. if it is a `Function`) the
 converted to one with [`pyfunc`](@ref PythonCall.pyfunc). In particular this means the arguments passed to it
 are always of type `Py`.
 """
-pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing) =
+pyproperty(; get=nothing, set=nothing, del=nothing, doc=nothing, kw...) =
     pybuiltins.property(
-        fget = ispy(get) || get === nothing ? get : pyfunc(get),
-        fset = ispy(set) || set === nothing ? set : pyfunc(set),
-        fdel = ispy(del) || del === nothing ? del : pyfunc(del),
+        fget = ispy(get) || get === nothing ? get : pyfunc(get; kw...),
+        fset = ispy(set) || set === nothing ? set : pyfunc(set; kw...),
+        fdel = ispy(del) || del === nothing ? del : pyfunc(del; kw...),
         doc = doc,
     )
-pyproperty(get) = pyproperty(get=get)
+pyproperty(get, set=nothing, del=nothing; doc=nothing, kw...) = pyproperty(; get=get, set=set, del=del, doc=doc, kw...)
 export pyproperty

From 4931e79c42bcff311fb481a1940538e432a189ea Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 5 May 2024 09:59:23 +0100
Subject: [PATCH 11/39] change to pyjl(x)

---
 docs/src/conversion-to-python.md |  8 -----
 docs/src/pythoncall-reference.md |  4 ++-
 docs/src/releasenotes.md         |  8 +++--
 pysrc/juliacall/__init__.py      |  9 -----
 src/JlWrap/any.jl                | 57 ++++++++++----------------------
 src/JlWrap/array.jl              | 19 +++++++++--
 src/JlWrap/base.jl               |  4 +--
 src/JlWrap/callback.jl           |  2 +-
 src/JlWrap/dict.jl               | 10 +++++-
 src/JlWrap/io.jl                 |  2 +-
 src/JlWrap/set.jl                | 10 +++++-
 src/JlWrap/vector.jl             |  2 +-
 12 files changed, 66 insertions(+), 69 deletions(-)

diff --git a/docs/src/conversion-to-python.md b/docs/src/conversion-to-python.md
index d95ad269..ac121518 100644
--- a/docs/src/conversion-to-python.md
+++ b/docs/src/conversion-to-python.md
@@ -44,11 +44,3 @@ object, then also define `ispy(::T) = true`.
 ```@docs
 PythonCall.ispy
 ```
-
-Alternatively, if you define a wrapper type (a subtype of
-[`juliacall.Jl`](#juliacall.Jl)) then you may instead define `pyjltype(::T)` to
-be that type.
-
-```@docs
-PythonCall.pyjltype
-```
diff --git a/docs/src/pythoncall-reference.md b/docs/src/pythoncall-reference.md
index 75065f50..e0a42d4b 100644
--- a/docs/src/pythoncall-reference.md
+++ b/docs/src/pythoncall-reference.md
@@ -88,11 +88,13 @@ conversion to Python, unless the value is immutable and has a corresponding Pyth
 
 ```@docs
 pyjl
-pyjlraw
 pyisjl
 pyjlvalue
 pybinaryio
 pytextio
+pyjlarray
+pyjlset
+pyjldict
 ```
 
 ## Arithmetic
diff --git a/docs/src/releasenotes.md b/docs/src/releasenotes.md
index 77158e67..55526456 100644
--- a/docs/src/releasenotes.md
+++ b/docs/src/releasenotes.md
@@ -6,9 +6,13 @@
   * 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.
+  * `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()`
+  * 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`.
 
 ## 0.9.20 (2024-05-01)
 * The IPython extension is now automatically loaded upon import if IPython is detected.
diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py
index 66bc34e5..286ce5b9 100644
--- a/pysrc/juliacall/__init__.py
+++ b/pysrc/juliacall/__init__.py
@@ -12,15 +12,6 @@ def newmodule(name):
         _newmodule = Main.seval("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/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 9aeebe9e..1b5390cb 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -1,7 +1,5 @@
 const pyjlanytype = pynew()
 
-pyjlany(x) = pyjl(pyjlanytype, x)
-
 # pyjlany_repr(self) = Py("<jl $(repr(self))>")
 function pyjlany_repr(self)
     str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit=>true, :displaysize=>(23,80)))
@@ -20,7 +18,7 @@ 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_)
-    pyjlany(getproperty(self, k))
+    pyjl(getproperty(self, k))
 end
 pyjl_handle_error_type(::typeof(pyjlany_getattr), self, exc) = pybuiltins.AttributeError
 
@@ -50,12 +48,12 @@ function pyjlany_call(self, args_::Py, kwargs_::Py)
     if pylen(kwargs_) > 0
         args = pyconvert(Vector{Any}, args_)
         kwargs = pyconvert(Dict{Symbol,Any}, kwargs_)
-        ans = pyjlany(self(args...; kwargs...))
+        ans = pyjl(self(args...; kwargs...))
     elseif pylen(args_) > 0
         args = pyconvert(Vector{Any}, args_)
-        ans = pyjlany(self(args...))
+        ans = pyjl(self(args...))
     else
-        ans = pyjlany(self())
+        ans = pyjl(self())
     end
     pydel!(args_)
     pydel!(kwargs_)
@@ -85,19 +83,19 @@ function pyjlany_getitem(self, k_::Py)
         if pyistuple(k_)
             k = pyconvert(Vector{Any}, k_)
             pydel!(k_)
-            pyjlany(self{k...})
+            pyjl(self{k...})
         else
             k = pyconvert(Any, k_)
-            pyjlany(self{k})
+            pyjl(self{k})
         end    
     else
         if pyistuple(k_)
             k = pyconvert(Vector{Any}, k_)
             pydel!(k_)
-            pyjlany(self[k...])
+            pyjl(self[k...])
         else
             k = pyconvert(Any, k_)
-            pyjlany(self[k])
+            pyjl(self[k])
         end
     end
 end
@@ -136,7 +134,7 @@ pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = exc isa MethodEr
 struct pyjlany_op{OP}
     op :: OP
 end
-(op::pyjlany_op)(self) = pyjlany(op.op(self))
+(op::pyjlany_op)(self) = pyjl(op.op(self))
 function (op::pyjlany_op)(self, other_::Py)
     if pyisjl(other_)
         other = pyjlvalue(other_)
@@ -144,7 +142,7 @@ function (op::pyjlany_op)(self, other_::Py)
     else
         other = pyconvert(Any, other_)
     end
-    pyjlany(op.op(self, other))
+    pyjl(op.op(self, other))
 end
 function (op::pyjlany_op)(self, other_::Py, other2_::Py)
     if pyisjl(other_)
@@ -157,7 +155,7 @@ function (op::pyjlany_op)(self, other_::Py, other2_::Py)
         other2 = pyjlvalue(other2_)
         pydel!(other2_)
     end
-    pyjlany(op.op(self, other, other2))
+    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
 
@@ -171,7 +169,7 @@ function (op::pyjlany_rev_op)(self, other_::Py)
     else
         other = pyconvert(Any, other_)
     end
-    pyjlany(op.op(other, self))
+    pyjl(op.op(other, self))
 end
 function (op::pyjlany_rev_op)(self, other_::Py, other2_::Py)
     if pyisjl(other_)
@@ -184,7 +182,7 @@ function (op::pyjlany_rev_op)(self, other_::Py, other2_::Py)
         other2 = pyjlvalue(other2_)
         pydel!(other2_)
     end
-    pyjlany(op.op(other, self, other2))
+    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
 
@@ -279,7 +277,7 @@ pyjlany_round(self) = pyint(round(Integer, self))
 function pyjlany_round(self, ndigits_::Py)
     ndigits = pyconvertarg(Int, ndigits_, "ndigits")
     pydel!(ndigits_)
-    pyjlany(round(self; digits = ndigits))
+    pyjl(round(self; digits = ndigits))
 end
 pyjl_handle_error_type(::typeof(pyjlany_round), self, exc::MethodError) = pybuiltins.TypeError
 
@@ -317,7 +315,7 @@ function pyjlany_next(self)
         errset(pybuiltins.StopIteration)
         PyNULL
     else
-        pyjlany(s[1])
+        pyjl(s[1])
     end
 end
 
@@ -486,28 +484,9 @@ function init_any()
 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.
+    pyjl(x)
 
-To define a custom conversion for your type `T`, overload `pyjltype(::T)`.
+Create a Python `juliacall.Jl` object wrapping the Julia object `x`.
 """
-pyjl(v) = pyjl(pyjltype(v), v)
+pyjl(v) = pyjl(pyjlanytype, v)
 export pyjl
-
-"""
-    pyjltype(x)
-
-The subtype of `juliacall.Jl` which the Julia object `x` is wrapped as by `pyjl(x)`.
-
-Overload `pyjltype(::T)` to define a custom conversion for your type `T`.
-"""
-pyjltype(::Any) = pyjlanytype
-export pyjltype
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index cb27c07b..016e2cb0 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -266,8 +266,8 @@ pytypestrdescr(::Type{T}) where {T} = get!(PYTYPESTRDESCR, T) do
     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))
@@ -342,4 +342,17 @@ function init_array()
     pycopy!(pyjlarraytype, jl.ArrayValue)
 end
 
-pyjltype(::AbstractArray) = pyjlarraytype
+"""
+    pyjlarray(x::AbstractArray)
+
+Wrap `x` as a Python array-like object.
+
+This object can be converted to a Numpy array with `numpy.array(v)`, `v.to_numpy()` or
+`v.__array__()` and supports these Numpy attributes: `ndim`, `shape`, `copy`, `reshape`.
+
+If `x` is one-dimensional (an `AbstractVector`) then it also behaves as a `list`.
+"""
+pyjlarray(x::AbstractArray) = pyjl(pyjlarraytype, x)
+export pyjlarray
+
+Py(x::AbstractArray) = pyjlarray(x)
diff --git a/src/JlWrap/base.jl b/src/JlWrap/base.jl
index 63caf20f..9b61abef 100644
--- a/src/JlWrap/base.jl
+++ b/src/JlWrap/base.jl
@@ -73,7 +73,7 @@ function Cjl._pyjl_callmethod(f, self_::C.PyPtr, args_::C.PyPtr, nargs::C.Py_ssi
                 if in_f
                     return pyjl_handle_error(f, self, exc)
                 else
-                    errset(pyJuliaError, pytuple((pyjlany(exc), pyjlany(catch_backtrace()))))
+                    errset(pyJuliaError, pytuple((pyjl(exc), pyjl(catch_backtrace()))))
                     return C.PyNULL
                 end
             catch
@@ -89,7 +89,7 @@ function pyjl_handle_error(f, self, exc)
     t = pyjl_handle_error_type(f, self, exc)::Py
     if pyisnull(t)
         # NULL => raise JuliaError
-        errset(pyJuliaError, pytuple((pyjlany(exc), pyjlany(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 2070a57c..4b7fcb57 100644
--- a/src/JlWrap/callback.jl
+++ b/src/JlWrap/callback.jl
@@ -1,6 +1,6 @@
 const pywrapcallback = pynew()
 
-pyjlcallback(f) = pyjlany(f).jl_callback
+pyjlcallback(f) = pyjl(f).jl_callback
 
 """
     pyfunc(f; [name], [qualname], [doc], [signature])
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index 6df2cc0b..1590800e 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -107,4 +107,12 @@ function init_dict()
     pycopy!(pyjldicttype, jl.DictValue)
 end
 
-pyjltype(::AbstractDict) = pyjldicttype
+"""
+    pyjldict(x::AbstractDict)
+
+Wrap `x` as a Python `dict`-like object.
+"""
+pyjldict(x::AbstractDict) = pyjl(pyjldicttype, x)
+export pyjldict
+
+Py(x::AbstractDict) = pyjldict(x)
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index 1dfe3e6c..14099569 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -319,4 +319,4 @@ Wrap `io` as a Python text IO object.
 pytextio(v::IO) = pyjl(pyjltextiotype, v)
 export pytextio
 
-pyjltype(::IO) = pyjlbinaryiotype
+Py(x::IO) = pybinaryio(x)
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index 2694d13a..a2157bc4 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -122,4 +122,12 @@ function init_set()
     pycopy!(pyjlsettype, jl.SetValue)
 end
 
-pyjltype(::AbstractSet) = pyjlsettype
+"""
+    pyjlset(x::AbstractSet)
+
+Wrap `x` as a Python `set`-like object.
+"""
+pyjlset(x::AbstractSet) = pyjl(pyjlsettype, x)
+export pyjlset
+
+Py(x::AbstractSet) = pyjlset(x)
diff --git a/src/JlWrap/vector.jl b/src/JlWrap/vector.jl
index 9f728625..45b9ca63 100644
--- a/src/JlWrap/vector.jl
+++ b/src/JlWrap/vector.jl
@@ -154,4 +154,4 @@ function init_vector()
     pycopy!(pyjlvectortype, jl.VectorValue)
 end
 
-pyjltype(::AbstractVector) = pyjlvectortype
+pyjlarray(x::AbstractVector) = pyjl(pyjlvectortype, x)

From 398c4ff5646d11c33178e936ab771ca44fda933e Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 5 May 2024 10:14:53 +0100
Subject: [PATCH 12/39] rename ArrayValue to JlArray etc

---
 docs/src/conversion-to-python.md |   6 +-
 docs/src/faq.md                  |   2 +-
 docs/src/juliacall-reference.md  | 123 ++++++-------------------------
 docs/src/pycall.md               |   2 +-
 src/JlWrap/array.jl              |   4 +-
 src/JlWrap/dict.jl               |   6 +-
 src/JlWrap/io.jl                 |  18 ++---
 src/JlWrap/set.jl                |   6 +-
 src/JlWrap/vector.jl             |   6 +-
 9 files changed, 48 insertions(+), 125 deletions(-)

diff --git a/docs/src/conversion-to-python.md b/docs/src/conversion-to-python.md
index ac121518..8333d900 100644
--- a/docs/src/conversion-to-python.md
+++ b/docs/src/conversion-to-python.md
@@ -24,9 +24,9 @@ From Python, this occurs when converting the return value of a Julia function.
 | `Date`, `Time`, `DateTime` (from `Dates`)                           | `date`, `time`, `datetime` (from `datetime`)            |
 | `Second`, `Millisecond`, `Microsecond`, `Nanosecond` (from `Dates`) | `timedelta` (from `datetime`)                           |
 | `Number`                                                            | `juliacall.NumberValue`, `juliacall.ComplexValue`, etc. |
-| `AbstractArray`                                                     | `juliacall.ArrayValue`, `juliacall.VectorValue`         |
-| `AbstractDict`                                                      | `juliacall.DictValue`                                   |
-| `AbstractSet`                                                       | `juliacall.SetValue`                                    |
+| `AbstractArray`                                                     | `juliacall.JlArray`, `juliacall.JlVector`         |
+| `AbstractDict`                                                      | `juliacall.JlDict`                                   |
+| `AbstractSet`                                                       | `juliacall.JlSet`                                    |
 | `IO`                                                                | `juliacall.BufferedIOValue`                             |
 | `Module`                                                            | `juliacall.ModuleValue`                                 |
 | `Type`                                                              | `juliacall.TypeValue`                                   |
diff --git a/docs/src/faq.md b/docs/src/faq.md
index 1cbcb207..1d3532c8 100644
--- a/docs/src/faq.md
+++ b/docs/src/faq.md
@@ -20,7 +20,7 @@ Related issues: [#201](https://github.com/JuliaPy/PythonCall.jl/issues/201), [#2
 
 ## Issues when Numpy arrays are expected
 
-When a Julia array is passed to Python, it is wrapped as a [`ArrayValue`](#juliacall.ArrayValue).
+When a Julia array is passed to Python, it is wrapped as a [`JlArray`](#juliacall.JlArray).
 This type satisfies the Numpy array interface and the buffer protocol, so can be used in
 most places where a numpy array is valid.
 
diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md
index 6a1b40c5..916af967 100644
--- a/docs/src/juliacall-reference.md
+++ b/docs/src/juliacall-reference.md
@@ -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,39 +33,27 @@ 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 [`Jl`](#juliacall.Jl) object, which wraps the original value, but
-giving it a Pythonic interface.
-
-Subclasses of [`Jl`](#juliacall.Jl) 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.
+Python to a [`Jl`](#juliacall.Jl) object, which wraps the original value and gives it a
+Pythonic interface.
 
-There is also a [`RawValue`](#juliacall.RawValue) object, which gives a stricter
-"Julia-only" interface, documented below. These types all inherit from `JlBase`:
+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`
-  - [`RawValue`](#juliacall.RawValue)
   - [`Jl`](#juliacall.Jl)
-    - [`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)
+  - [`JlArray`](#juliacall.JlArray)
+    - [`JlVector`](#juliacall.JlVector)
+  - [`JlDict`](#juliacall.JlDict)
+  - [`JlSet`](#juliacall.JlSet)
+  - [`JlIOBase`](#juliacall.JlIOBase)
+    - `JlBinaryIO`
+    - `JlTextIO`
 
 `````@customdoc
 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)`.
@@ -92,24 +68,13 @@ naming conventions, `_b` at the end of an attribute is replaced with `!` and `_b
 replaced with `!!`.
 
 ###### Members
-- `_jl_raw()`: Convert to a [`RawValue`](#juliacall.RawValue). (See also [`pyjlraw`](@ref).)
-- `_jl_display()`: Display the object using Julia's display mechanism.
-- `_jl_help()`: Display help for the object.
+- `jl_raw()`: Convert to a [`RawValue`](#juliacall.RawValue). (See also [`pyjlraw`](@ref).)
+- `jl_display()`: Display the object using Julia's display mechanism.
+- `jl_help()`: Display help for the object.
 `````
 
 `````@customdoc
-juliacall.NumberValue - Class
-
-This wraps any Julia `Number` value. It is a subclass of `numbers.Number` and behaves
-similar to other Python numbers.
-
-There are also subtypes `ComplexValue`, `RealValue`, `RationalValue`, `IntegerValue` which
-wrap values of the corresponding Julia types, and are subclasses of the corresponding
-`numbers` ABC.
-`````
-
-`````@customdoc
-juliacall.ArrayValue - Class
+juliacall.JlArray - Class
 
 This wraps any Julia `AbstractArray` value. It is a subclass of
 `collections.abc.Collection`.
@@ -136,9 +101,9 @@ copy of the original 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
@@ -156,65 +121,23 @@ This wraps any Julia `AbstractVector` value. It is a subclass of `juliacall.Arra
 `````
 
 `````@customdoc
-juliacall.DictValue - Class
+juliacall.JlDict - Class
 This wraps any Julia `AbstractDict` value. It is a subclass of `collections.abc.Mapping` and
 behaves similar to a Python `dict`.
 `````
 
 `````@customdoc
-juliacall.SetValue - Class
+juliacall.JlSet - Class
 This wraps any Julia `AbstractSet` value. It is a subclass of `collections.abc.Set` and
 behaves similar to a Python `set`.
 `````
 
 `````@customdoc
-juliacall.IOValue - Class
+juliacall.JlIOBase - Class
 
 This wraps any Julia `IO` value. It is a subclass of `io.IOBase` and behaves like Python
 files.
 
-There are also subtypes `BinaryIOValue` and `TextIOValue`, which are subclasses of
+There are also subtypes `JlBinaryIO` and `JlTextIO`, which are subclasses of
 `io.BufferedIOBase` (buffered bytes) and `io.TextIOBase` (text).
 `````
-
-`````@customdoc
-juliacall.ModuleValue - Class
-This wraps any Julia `Module` value.
-
-It is the same as [`Jl`](#juliacall.Jl) 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 [`Jl`](#juliacall.Jl) 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 [`Jl`](#juliacall.Jl) 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 [`Jl`](#juliacall.Jl) (or subclass). (See also
-  [`pyjl`](@ref).)
-`````
diff --git a/docs/src/pycall.md b/docs/src/pycall.md
index e96f89dd..740b89ff 100644
--- a/docs/src/pycall.md
+++ b/docs/src/pycall.md
@@ -26,7 +26,7 @@ Furthermore, `pyconvert` can be extended to support more types, whereas `convert
 
 Both packages allow conversion of Julia values to Python: `PyObject(x)` in PyCall, `Py(x)` in PythonCall.
 
-Whereas both packages convert numbers, booleans, tuples and strings to their Python counterparts, they differ in handling other types. For example PyCall converts `AbstractVector` to `list` whereas PythonCall converts `AbstractVector` to `juliacall.VectorValue` which is a sequence type directly wrapping the Julia value - this has the advantage that mutating the Python object also mutates the original Julia object.
+Whereas both packages convert numbers, booleans, tuples and strings to their Python counterparts, they differ in handling other types. For example PyCall converts `AbstractVector` to `list` whereas PythonCall converts `AbstractVector` to `juliacall.JlVector` which is a sequence type directly wrapping the Julia value - this has the advantage that mutating the Python object also mutates the original Julia object.
 
 Hence with PyCall the following does not mutate the original array `x`:
 ```julia
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 016e2cb0..83181a0d 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -295,7 +295,7 @@ function init_array()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class ArrayValue(JlBase):
+    class JlArray(JlBase):
         __slots__ = ()
         _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info))
         @property
@@ -339,7 +339,7 @@ function init_array()
             import numpy
             return numpy.array(self, dtype=dtype, copy=copy, order=order)
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjlarraytype, jl.ArrayValue)
+    pycopy!(pyjlarraytype, jl.JlArray)
 end
 
 """
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index 1590800e..a843550a 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -35,7 +35,7 @@ function init_dict()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class DictValue(JlBase):
+    class JlDict(JlBase):
         __slots__ = ()
         _jl_undefined_ = object()
         def __bool__(self):
@@ -101,10 +101,10 @@ function init_dict()
         def copy(self):
             return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy)))
     import collections.abc
-    collections.abc.MutableMapping.register(DictValue)
+    collections.abc.MutableMapping.register(JlDict)
     del collections
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjldicttype, jl.DictValue)
+    pycopy!(pyjldicttype, jl.JlDict)
 end
 
 """
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index 14099569..0642f20e 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -205,7 +205,7 @@ function init_io()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class IOValueBase(JlBase):
+    class JlIOBase(JlBase):
         __slots__ = ()
         def close(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlio_close)))
@@ -256,7 +256,7 @@ function init_io()
                 return line
             else:
                 raise StopIteration
-    class BinaryIOValue(IOValueBase):
+    class JlBinaryIO(JlIOBase):
         __slots__ = ()
         def detach(self):
             raise ValueError("Cannot detach '{}'.".format(type(self)))
@@ -272,7 +272,7 @@ function init_io()
             return self.readinto(b)
         def write(self, b):
             return self._jl_callmethod($(pyjl_methodnum(pyjlbinaryio_write)), b)
-    class TextIOValue(IOValueBase):
+    class JlTextIO(JlIOBase):
         __slots__ = ()
         @property
         def encoding(self):
@@ -289,14 +289,14 @@ function init_io()
         def write(self, s):
             return self._jl_callmethod($(pyjl_methodnum(pyjltextio_write)), s)
     import io
-    io.IOBase.register(IOValueBase)
-    io.BufferedIOBase.register(BinaryIOValue)
-    io.TextIOBase.register(TextIOValue)
+    io.IOBase.register(JlIOBase)
+    io.BufferedIOBase.register(JlBinaryIO)
+    io.TextIOBase.register(JlTextIO)
     del io
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjliobasetype, jl.IOValueBase)
-    pycopy!(pyjlbinaryiotype, jl.BinaryIOValue)
-    pycopy!(pyjltextiotype, jl.TextIOValue)
+    pycopy!(pyjliobasetype, jl.JlIOBase)
+    pycopy!(pyjlbinaryiotype, jl.JlBinaryIO)
+    pycopy!(pyjltextiotype, jl.JlTextIO)
 end
 
 pyiobase(v::IO) = pyjl(pyjliobasetype, v)
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index a2157bc4..624e33ce 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -77,7 +77,7 @@ function init_set()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class SetValue(JlBase):
+    class JlSet(JlBase):
         __slots__ = ()
         def __bool__(self):
             return bool(len(self))
@@ -116,10 +116,10 @@ function init_set()
         def update(self, other):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_update)), other)
     import collections.abc
-    collections.abc.MutableSet.register(SetValue)
+    collections.abc.MutableSet.register(JlSet)
     del collections
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjlsettype, jl.SetValue)
+    pycopy!(pyjlsettype, jl.JlSet)
 end
 
 """
diff --git a/src/JlWrap/vector.jl b/src/JlWrap/vector.jl
index 45b9ca63..18f76ba6 100644
--- a/src/JlWrap/vector.jl
+++ b/src/JlWrap/vector.jl
@@ -121,7 +121,7 @@ function init_vector()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class VectorValue(ArrayValue):
+    class JlVector(JlArray):
         __slots__ = ()
         def resize(self, size):
             return self._jl_callmethod($(pyjl_methodnum(pyjlvector_resize)), size)
@@ -148,10 +148,10 @@ function init_vector()
         def count(self, value):
             return self._jl_callmethod($(pyjl_methodnum(pyjlvector_count)), value)
     import collections.abc
-    collections.abc.MutableSequence.register(VectorValue)
+    collections.abc.MutableSequence.register(JlVector)
     del collections
     """, @__FILE__(), "exec"), jl.__dict__)
-    pycopy!(pyjlvectortype, jl.VectorValue)
+    pycopy!(pyjlvectortype, jl.JlVector)
 end
 
 pyjlarray(x::AbstractVector) = pyjl(pyjlvectortype, x)

From f82be5695a7732a7acd3db0c9b5f94be5ee66a75 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 5 May 2024 11:28:20 +0100
Subject: [PATCH 13/39] __init__ for all

---
 src/JlWrap/C.jl      | 25 ++++++++++++++++++++++---
 src/JlWrap/array.jl  |  2 ++
 src/JlWrap/dict.jl   |  4 ++++
 src/JlWrap/io.jl     |  2 ++
 src/JlWrap/set.jl    |  2 ++
 src/JlWrap/vector.jl |  4 ++++
 6 files changed, 36 insertions(+), 3 deletions(-)

diff --git a/src/JlWrap/C.jl b/src/JlWrap/C.jl
index a4ac547b..e4215ac3 100644
--- a/src/JlWrap/C.jl
+++ b/src/JlWrap/C.jl
@@ -15,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
@@ -88,8 +89,9 @@ function _pyjl_init(xptr::C.PyPtr, argsptr::C.PyPtr, kwargsptr::C.PyPtr)
         PyJuliaValue_SetValue(xptr, v)
         Cint(0)
     catch exc
-        C.PyErr_SetString(C.POINTERS.PyExc_Exception, "error during __init__()")
-        @debug "exception" 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
@@ -354,6 +356,12 @@ function init_c()
         C.PyErr_Print()
         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__()
@@ -419,7 +427,18 @@ function PyJuliaValue_New(t::C.PyPtr, @nospecialize(v))
         C.PyErr_SetString(C.POINTERS.PyExc_TypeError, "Expecting a subtype of 'juliacall.JlBase'")
         return C.PyNULL
     end
-    o = C.PyObject_CallObject(t, C.PyNULL)
+    # All of this just to do JuliaBase.__new__(t). We do this to avoid calling `__init__`
+    # which itself sets the value, and so duplicates work. Some classes such as `JlArray` do
+    # not allow calling `__init__` with no args.
+    # TODO: it could be replaced with PyObject_CallOneArg(PyJuliaBase_New[], t) when we drop
+    # support for Python 3.8.
+    args = C.PyTuple_New(1)
+    args == C.PyNULL && return C.PyNULL
+    C.Py_IncRef(t)
+    err = C.PyTuple_SetItem(args, 0, t)
+    err == -1 && (C.Py_DecRef(args); return C.PyNULL)
+    o = C.PyObject_CallObject(PyJuliaBase_New[], args)
+    C.Py_DecRef(args)
     o == C.PyNULL && return C.PyNULL
     PyJuliaValue_SetValue(o, v)
     return o
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 83181a0d..0fa525ae 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -298,6 +298,8 @@ function init_array()
     class JlArray(JlBase):
         __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)))
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index a843550a..e5b69661 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -38,6 +38,10 @@ function init_dict()
     class JlDict(JlBase):
         __slots__ = ()
         _jl_undefined_ = object()
+        def __init__(self, value=None):
+            if value is None:
+                value = Base.Dict()
+            JlBase.__init__(self, value, Base.AbstractDict)
         def __bool__(self):
             return bool(len(self))
         def __iter__(self):
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index 0642f20e..c694186f 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -207,6 +207,8 @@ function init_io()
     $("\n"^(@__LINE__()-1))
     class JlIOBase(JlBase):
         __slots__ = ()
+        def __init__(self, value):
+            JlBase.__init__(self, value, Base.IO)
         def close(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlio_close)))
         @property
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index 624e33ce..249d4240 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -79,6 +79,8 @@ function init_set()
     $("\n"^(@__LINE__()-1))
     class JlSet(JlBase):
         __slots__ = ()
+        def __init__(self, value=None):
+            JlBase.__init__(self, value, Base.AbstractSet)
         def __bool__(self):
             return bool(len(self))
         def add(self, value):
diff --git a/src/JlWrap/vector.jl b/src/JlWrap/vector.jl
index 18f76ba6..b78bd0e0 100644
--- a/src/JlWrap/vector.jl
+++ b/src/JlWrap/vector.jl
@@ -123,6 +123,10 @@ function init_vector()
     $("\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):

From 747a3310e4259d67aaa020cb05a228e05556dbc3 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 5 May 2024 12:41:44 +0100
Subject: [PATCH 14/39] mixins for consistency

---
 src/JlWrap/any.jl   | 44 +++++++++++++++++++++++++++++++++++++++-----
 src/JlWrap/array.jl |  6 +++---
 src/JlWrap/dict.jl  | 12 ++++--------
 src/JlWrap/io.jl    |  2 +-
 src/JlWrap/set.jl   |  8 ++++----
 5 files changed, 51 insertions(+), 21 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 1b5390cb..4d2015e4 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -1,11 +1,12 @@
 const pyjlanytype = pynew()
+const pyjlitertype = pynew()
 
 # pyjlany_repr(self) = Py("<jl $(repr(self))>")
 function pyjlany_repr(self)
     str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit=>true, :displaysize=>(23,80)))
     # type = self isa Function ? "Function" : self isa Type ? "Type" : nameof(typeof(self))
     sep = '\n' in str ? '\n' : ' '
-    Py("Julia:$sep$str"::String)
+    Py("$(sep)$(str)"::String)
 end
 
 # Note: string(self) doesn't always return a String
@@ -319,16 +320,48 @@ function pyjlany_next(self)
     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 Jl(JlBase):
+    class _JlReprMixin:
         __slots__ = ()
         def __repr__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
+            t = type(self)
+            if t is Jl:
+                name = "Julia"
+            else:
+                name = t.__name__
+            return name + ":" + self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
+    class _JlHashMixin:
+        __slots__ = ()
+        def __hash__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyint ∘ hash)))
+    class JlIter(JlBase, _JlReprMixin, _JlHashMixin):
+        def __iter__(self):
+            return self
+        def __next__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjliter_next)))
+    class _JlContainerMixin(_JlReprMixin, _JlHashMixin):
+        __slots__ = ()
+        def __len__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyint ∘ length)))
+        def __bool__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pybool ∘ !isempty)))
+    class Jl(JlBase, _JlReprMixin, _JlHashMixin):
+        __slots__ = ()
         def __str__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_str)))
         def __getattr__(self, k):
@@ -443,8 +476,6 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(≥))), other)
         def __gt__(self, other):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(>))), other)
-        def __hash__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash)))
         def __int__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_int)))
         def __float__(self):
@@ -481,6 +512,7 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_mimebundle)), include, exclude)
     """, @__FILE__(), "exec"), jl.__dict__)
     pycopy!(pyjlanytype, jl.Jl)
+    pycopy!(pyjlitertype, jl.JlIter)
 end
 
 """
@@ -490,3 +522,5 @@ Create a Python `juliacall.Jl` object wrapping the Julia object `x`.
 """
 pyjl(v) = pyjl(pyjlanytype, v)
 export pyjl
+
+pyjliter(x) = pyjl(pyjlitertype, x)
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 0fa525ae..4af140c8 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -295,7 +295,7 @@ function init_array()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlArray(JlBase):
+    class JlArray(JlBase, _JlContainerMixin):
         __slots__ = ()
         _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info))
         def __init__(self, value):
@@ -310,14 +310,14 @@ function init_array()
             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)
+        def __iter__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator)))
         @property
         def __array_interface__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlarray_array_interface)))
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index e5b69661..66586ddf 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -9,8 +9,6 @@ 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))
@@ -35,17 +33,15 @@ function init_dict()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlDict(JlBase):
+    class JlDict(JlBase, _JlContainerMixin):
         __slots__ = ()
         _jl_undefined_ = object()
         def __init__(self, value=None):
             if value is None:
                 value = Base.Dict()
             JlBase.__init__(self, value, Base.AbstractDict)
-        def __bool__(self):
-            return bool(len(self))
         def __iter__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjldict_iter)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator ∘ keys)))
         def __contains__(self, key):
             return self._jl_callmethod($(pyjl_methodnum(pyjldict_contains)), key)
         def __getitem__(self, key):
@@ -61,11 +57,11 @@ function init_dict()
             else:
                 raise KeyError(key)
         def keys(self):
-            return self._jl_callmethod($(pyjl_methodnum(Py ∘ keys)))
+            return self._jl_callmethod($(pyjl_methodnum(pyset ∘ keys)))
         def values(self):
             return self._jl_callmethod($(pyjl_methodnum(Py ∘ values)))
         def items(self):
-            return self._jl_callmethod($(pyjl_methodnum(Py ∘ DictPairSet)))
+            return self._jl_callmethod($(pyjl_methodnum(pyset ∘ DictPairSet)))
         def get(self, key, default=None):
             if key in self:
                 return self[key]
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index c694186f..a0d77afb 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -205,7 +205,7 @@ function init_io()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlIOBase(JlBase):
+    class JlIOBase(JlBase, _JlReprMixin, _JlHashMixin):
         __slots__ = ()
         def __init__(self, value):
             JlBase.__init__(self, value, Base.IO)
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index 249d4240..a3e7b805 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -77,12 +77,12 @@ function init_set()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlSet(JlBase):
+    class JlSet(JlBase, _JlContainerMixin):
         __slots__ = ()
         def __init__(self, value=None):
             JlBase.__init__(self, value, Base.AbstractSet)
-        def __bool__(self):
-            return bool(len(self))
+        def __iter__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator)))
         def add(self, value):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_add)), value)
         def discard(self, value):
@@ -90,7 +90,7 @@ function init_set()
         def clear(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_clear)))
         def copy(self):
-            return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ copy)))
         def pop(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_pop)))
         def remove(self, value):

From 526f0449643a6c429cf24810de0b1d7835ca6947 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Sun, 12 May 2024 20:10:36 +0100
Subject: [PATCH 15/39] improve pymacro docstring

---
 src/PyMacro/PyMacro.jl | 132 +++++++++++++++++++++--------------------
 1 file changed, 69 insertions(+), 63 deletions(-)

diff --git a/src/PyMacro/PyMacro.jl b/src/PyMacro/PyMacro.jl
index 04bf9718..87087221 100644
--- a/src/PyMacro/PyMacro.jl
+++ b/src/PyMacro/PyMacro.jl
@@ -66,13 +66,13 @@ const PY_MACRO_UNOPS = Dict(
     :tuple => (pytuple, true),
     :type => (pytype, true),
     # builtins converting to julia
-    :jlascii => (x->pyascii(String,x), false),
+    :jlascii => (x -> pyascii(String, x), false),
     :jlbool => (pytruth, false),
-    :jlbytes => (x->pybytes(Base.CodeUnits,x), false),
+    :jlbytes => (x -> pybytes(Base.CodeUnits, x), false),
     :jlhash => (pyhash, false),
     :jllen => (pylen, false),
-    :jlrepr => (x->pyrepr(String,x), false),
-    :jlstr => (x->pystr(String,x), false),
+    :jlrepr => (x -> pyrepr(String, x), false),
+    :jlstr => (x -> pystr(String, x), false),
     # jlcomplex
 )
 
@@ -92,13 +92,13 @@ const PY_MACRO_BINOPS = Dict(
     :(⊻) => (pyxor, true),
     :(==) => (pyeq, true),
     :(!=) => (pyne, true),
-    :(≠ ) => (pyne, true),
+    :(≠) => (pyne, true),
     :(<=) => (pyle, true),
-    :(≤ ) => (pyle, true),
-    :(< ) => (pylt, true),
+    :(≤) => (pyle, true),
+    :(<) => (pylt, true),
     :(>=) => (pyge, true),
-    :(≥ ) => (pyge, true),
-    :(> ) => (pygt, true),
+    :(≥) => (pyge, true),
+    :(>) => (pygt, true),
     :(===) => (pyis, false),
     :(≡) => (pyis, false),
     :(!==) => (pyisnot, false),
@@ -129,10 +129,10 @@ const PY_MACRO_TERNOPS = Dict(
 )
 
 Base.@kwdef mutable struct PyMacroState
-    mod :: Module
-    src :: LineNumberNode
-    consts :: IdDict{Any,Py} = IdDict{Any,Py}()
-    inits :: Vector{Any} = []
+    mod::Module
+    src::LineNumberNode
+    consts::IdDict{Any,Py} = IdDict{Any,Py}()
+    inits::Vector{Any} = []
 end
 
 function py_macro_err(st, ex, msg=nothing)
@@ -147,48 +147,51 @@ end
 
 py_macro_assign(body, ans, ex) = push!(body, :($ans = $ex))
 
-py_macro_del(body, var, tmp) = if tmp; push!(body, :($pydel!($var))); end
+py_macro_del(body, var, tmp) =
+    if tmp
+        push!(body, :($pydel!($var)))
+    end
 
 ismacroexpr(ex, name) = isexpr(ex, :macrocall) && (ex.args[1] === Symbol(name) || ex.args[1] === GlobalRef(Base.Core, Symbol(name)))
 
 function py_macro_lower(st, body, ans, ex; flavour=:expr)
 
     # scalar literals
-    if ex isa Union{Nothing, String, Bool, Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128, BigInt, Float16, Float32, Float64}
+    if ex isa Union{Nothing,String,Bool,Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UInt64,UInt128,BigInt,Float16,Float32,Float64}
         x = get!(pynew, st.consts, ex)
         py_macro_assign(body, ans, x)
         return false
 
-    # Int128 literals
+        # Int128 literals
     elseif ismacroexpr(ex, "@int128_str")
         value = parse(Int128, ex.args[3])
         x = get!(pynew, st.consts, value)
         py_macro_assign(body, ans, x)
         return false
 
-    # UInt128 literals
+        # UInt128 literals
     elseif ismacroexpr(ex, "@uint128_str")
         value = parse(UInt128, ex.args[3])
         x = get!(pynew, st.consts, value)
         py_macro_assign(body, ans, x)
         return false
 
-    # big integer literals
+        # big integer literals
     elseif ismacroexpr(ex, "@big_str")
         value = parse(BigInt, ex.args[3])
         x = get!(pynew, st.consts, value)
         py_macro_assign(body, ans, x)
         return false
 
-    # __file__
+        # __file__
     elseif ex === :__file__
         return py_macro_lower(st, body, ans, string(st.src.file))
 
-    # __line__
+        # __line__
     elseif ex === :__line__
         return py_macro_lower(st, body, ans, st.src.line)
 
-    # x
+        # x
     elseif ex isa Symbol
         if ex in BUILTINS
             py_macro_assign(body, ans, :($pybuiltins.$ex))
@@ -197,37 +200,37 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         end
         return false
 
-    # x:y:z
-    elseif flavour==:index && @capture(ex, ax_:ay_:az_)
+        # x:y:z
+    elseif flavour == :index && @capture(ex, ax_:ay_:az_)
         @gensym x y z
-        tx = py_macro_lower(st, body, x, ax===:_ ? :None : ax)
-        ty = py_macro_lower(st, body, y, ay===:_ ? :None : ay)
-        tz = py_macro_lower(st, body, z, az===:_ ? :None : az)
+        tx = py_macro_lower(st, body, x, ax === :_ ? :None : ax)
+        ty = py_macro_lower(st, body, y, ay === :_ ? :None : ay)
+        tz = py_macro_lower(st, body, z, az === :_ ? :None : az)
         py_macro_assign(body, ans, :($pyslice($x, $y, $z)))
         py_macro_del(body, x, tx)
         py_macro_del(body, y, ty)
         py_macro_del(body, z, tz)
         return true
 
-    # x:y
-    elseif flavour==:index && @capture(ex, ax_:ay_)
+        # x:y
+    elseif flavour == :index && @capture(ex, ax_:ay_)
         @gensym x y
-        tx = py_macro_lower(st, body, x, ax===:_ ? :None : ax)
-        ty = py_macro_lower(st, body, y, ay===:_ ? :None : ay)
+        tx = py_macro_lower(st, body, x, ax === :_ ? :None : ax)
+        ty = py_macro_lower(st, body, y, ay === :_ ? :None : ay)
         py_macro_assign(body, ans, :($pyslice($x, $y)))
         py_macro_del(body, x, tx)
         py_macro_del(body, y, ty)
         return true
 
-    # x + y + z + ...
+        # x + y + z + ...
     elseif @capture(ex, +(ax_, ay_, az_, args__))
-        return py_macro_lower(st, body, ans, foldl((x, y)->:($x+$y), (ax, ay, az, args...)))
+        return py_macro_lower(st, body, ans, foldl((x, y) -> :($x + $y), (ax, ay, az, args...)))
 
-    # x * y * z * ...
+        # x * y * z * ...
     elseif @capture(ex, *(ax_, ay_, az_, args__))
-        return py_macro_lower(st, body, ans, foldl((x, y)->:($x*$y), (ax, ay, az, args...)))
+        return py_macro_lower(st, body, ans, foldl((x, y) -> :($x * $y), (ax, ay, az, args...)))
 
-    # f(args...; kwargs...)
+        # f(args...; kwargs...)
     elseif isexpr(ex, :call)
         af = ex.args[1]
         # is it a special operator?
@@ -333,7 +336,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             return true
         end
 
-    # (...)
+        # (...)
     elseif isexpr(ex, :tuple)
         if any(isexpr(arg, :...) for arg in ex.args)
             py_macro_err(st, ex, "splatting into tuples not implemented")
@@ -341,14 +344,14 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             py_macro_assign(body, ans, :($pynulltuple($(length(ex.args)))))
             @gensym a
             for (i, aa) in enumerate(ex.args)
-                ta = py_macro_lower(st, body, a, aa, flavour = flavour==:index ? :index : :expr)
-                push!(body, :($pytuple_setitem($ans, $(i-1), $a)))
+                ta = py_macro_lower(st, body, a, aa, flavour=flavour == :index ? :index : :expr)
+                push!(body, :($pytuple_setitem($ans, $(i - 1), $a)))
                 py_macro_del(body, a, ta)
             end
             return true
         end
 
-    # [...]
+        # [...]
     elseif isexpr(ex, :vect)
         if any(isexpr(arg, :...) for arg in ex.args)
             py_macro_err(st, ex, "splatting into tuples not implemented")
@@ -357,13 +360,13 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             @gensym a
             for (i, aa) in enumerate(ex.args)
                 ta = py_macro_lower(st, body, a, aa)
-                push!(body, :($pylist_setitem($ans, $(i-1), $a)))
+                push!(body, :($pylist_setitem($ans, $(i - 1), $a)))
                 py_macro_del(body, a, ta)
             end
             return true
         end
 
-    # {...}
+        # {...}
     elseif isexpr(ex, :braces)
         # Like Python, we allow braces to be set or dict literals.
         #
@@ -398,7 +401,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
                     push!(body, :($pydict_setitem($ans, $k, $v)))
                     py_macro_del(body, k, tk)
                     py_macro_del(body, v, tv)
-                elseif @capture(aa, ak_ : av_)
+                elseif @capture(aa, ak_:av_)
                     tk = py_macro_lower(st, body, k, ak)
                     tv = py_macro_lower(st, body, v, av)
                     push!(body, :($pydict_setitem($ans, $k, $v)))
@@ -423,7 +426,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             return true
         end
 
-    # x.k
+        # x.k
     elseif isexpr(ex, :.)
         ax, ak = ex.args
         @gensym x k
@@ -438,7 +441,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_del(body, k, tk)
         return true
 
-    # x[k]
+        # x[k]
     elseif @capture(ex, ax_[ak__])
         @gensym x k
         tx = py_macro_lower(st, body, x, ax)
@@ -452,13 +455,13 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_del(body, k, tk)
         return true
 
-    # x = y
+        # x = y
     elseif @capture(ex, ax_ = ay_)
         ty = py_macro_lower(st, body, ans, ay)
         py_macro_lower_assign(st, body, ax, ans)
         return ty
 
-    # @del x, y, ...
+        # @del x, y, ...
     elseif @capture(ex, @del (args__,))
 
         for arg in args
@@ -477,7 +480,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
                 py_macro_del(body, x, tx)
                 py_macro_del(body, k, tk)
 
-            # @del x[k]
+                # @del x[k]
             elseif @capture(arg, ax_[ak__])
                 @gensym x k
                 tx = py_macro_lower(st, body, x, ax)
@@ -497,18 +500,18 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, nothing)
         return false
 
-    # @del x
+        # @del x
     elseif @capture(ex, @del arg_)
         return py_macro_lower(st, body, ans, :(@del ($arg,)))
 
-    # @jl x
+        # @jl x
     elseif @capture(ex, @jl ax_)
         y = py_macro_lower_jl(st, ax)
         py_macro_assign(body, ans, y)
         return false
 
-    # @compile code mode=mode ...
-    elseif @capture(ex, @compile code_String mode=mode_String args__)
+        # @compile code mode=mode ...
+    elseif @capture(ex, @compile code_String mode = mode_String args__)
         x = pynew()
         args = [isexpr(arg, :(=)) ? Expr(:kw, arg.args...) : arg for arg in args]
         filename = "$(st.src.file):$(st.src.line)"
@@ -516,7 +519,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, x)
         return false
 
-    # @exec code ...
+        # @exec code ...
     elseif @capture(ex, @exec code_String args__)
         args = [isexpr(arg, :(=)) ? Expr(:kw, arg.args...) : arg for arg in args]
         ex2 = Expr(:macrocall, Symbol("@compile"), st.src, code, :(mode = "exec"))
@@ -527,14 +530,14 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, nothing)
         return false
 
-    # @eval code ...
+        # @eval code ...
     elseif @capture(ex, @eval code_String args__)
         args = [isexpr(arg, :(=)) ? Expr(:kw, arg.args...) : arg for arg in args]
         ex2 = Expr(:macrocall, Symbol("@compile"), st.src, code, :(mode = "eval"))
         ex2 = Expr(:call, :eval, ex2, args...)
         return py_macro_lower(st, body, ans, ex2)
 
-    # begin; ...; end
+        # begin; ...; end
     elseif isexpr(ex, :block)
         if isempty(ex.args)
             py_macro_assign(body, ans, nothing)
@@ -560,7 +563,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             @assert false
         end
 
-    # if x; ...; end
+        # if x; ...; end
     elseif isexpr(ex, :if, :elseif)
         if length(ex.args) == 2
             return py_macro_lower(st, body, ans, Expr(ex.head, ex.args..., nothing))
@@ -581,7 +584,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
             return t
         end
 
-    # x && y
+        # x && y
     elseif isexpr(ex, :&&)
         ax, ay = ex.args
         tx = py_macro_lower(st, body, ans, ax)
@@ -597,7 +600,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         push!(body, Expr(:if, :($pytruth($ans)), Expr(:block, body2...), Expr(:block, body3...)))
         return t
 
-    # x || y
+        # x || y
     elseif isexpr(ex, :||)
         ax, ay = ex.args
         tx = py_macro_lower(st, body, ans, ax)
@@ -613,7 +616,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         push!(body, Expr(:if, :($pytruth($ans)), Expr(:block, body2...), Expr(:block, body3...)))
         return t
 
-    # while x; ...; end
+        # while x; ...; end
     elseif isexpr(ex, :while)
         ax, ay = ex.args
         @gensym x y
@@ -626,8 +629,10 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, nothing)
         return false
 
-    # for x in y; ...; end
-    elseif @capture(ex, for ax_ in ay_; az_; end)
+        # for x in y; ...; end
+    elseif @capture(ex, for ax_ in ay_
+        az_
+    end)
         @gensym y i v z
         ty = py_macro_lower(st, body, y, ay)
         push!(body, :($i = $pyiter($y)))
@@ -644,7 +649,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, nothing)
         return false
 
-    # import ...
+        # import ...
     elseif isexpr(ex, :import)
         for aa in ex.args
             if isexpr(aa, :as)
@@ -680,7 +685,7 @@ function py_macro_lower(st, body, ans, ex; flavour=:expr)
         py_macro_assign(body, ans, nothing)
         return false
 
-    # "...$foo..."
+        # "...$foo..."
     elseif isexpr(ex, :string)
         args = [a isa String ? a : :(str($a)) for a in ex.args]
         return py_macro_lower(st, body, ans, :("".join(($(args...),))))
@@ -795,8 +800,9 @@ Evaluate the given expression using Pythonic semantics.
 For example:
 - `f(x, y)` is translated to `pycall(f, x, y)`
 - `x + y` is translated to `pyadd(x, y)`
-- `x === y` is translated to `pyis(x, y)`
+- `x === y` is translated to `pyis(x, y)` (`x is y` in Python)
 - `x.foo` is translated to `pygetattr(x, "foo")`
+- `import x: f as g` is translated to `g = pyimport("x" => "f")` (`from x import f as g` in Python)
 
 Compound statements such as `begin`, `if`, `while` and `for` are supported.
 

From 49645fca94a35feada39431f963e32010574ba4d Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:41:18 +0100
Subject: [PATCH 16/39] jlwrap tests

---
 src/JlWrap/any.jl |  16 ++---
 test/Compat.jl    |   2 +-
 test/JlWrap.jl    | 178 +++++++++++++++++++++++-----------------------
 3 files changed, 97 insertions(+), 99 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 4d2015e4..3e21cde7 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -3,7 +3,7 @@ const pyjlitertype = pynew()
 
 # pyjlany_repr(self) = Py("<jl $(repr(self))>")
 function pyjlany_repr(self)
-    str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit=>true, :displaysize=>(23,80)))
+    str = repr(MIME("text/plain"), self; context=IOContext(devnull, :limit => true, :displaysize => (23, 80)))
     # type = self isa Function ? "Function" : self isa Type ? "Type" : nameof(typeof(self))
     sep = '\n' in str ? '\n' : ' '
     Py("$(sep)$(str)"::String)
@@ -35,12 +35,12 @@ pyjl_handle_error_type(::typeof(pyjlany_setattr), self, exc) = pybuiltins.Attrib
 function pyjlany_dir(self)
     ks = Symbol[]
     if self isa Module
-        append!(ks, names(self, all = true, imported = true))
+        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!(propertynames(self, true))
+        append!(ks, propertynames(self, true))
     end
     pylist(pyjl_attr_jl2py(string(k)) for k in ks)
 end
@@ -88,7 +88,7 @@ function pyjlany_getitem(self, k_::Py)
         else
             k = pyconvert(Any, k_)
             pyjl(self{k})
-        end    
+        end
     else
         if pyistuple(k_)
             k = pyconvert(Vector{Any}, k_)
@@ -133,7 +133,7 @@ pyjlany_contains(self, v::Py) = Py((@pyconvert(eltype(self), v, return Py(false)
 pyjl_handle_error_type(::typeof(pyjlany_contains), self, exc) = exc isa MethodError && exc.f === in ? pybuiltins.TypeError : PyNULL
 
 struct pyjlany_op{OP}
-    op :: OP
+    op::OP
 end
 (op::pyjlany_op)(self) = pyjl(op.op(self))
 function (op::pyjlany_op)(self, other_::Py)
@@ -161,7 +161,7 @@ end
 pyjl_handle_error_type(op::pyjlany_op, self, exc) = exc isa MethodError && exc.f === op.op ? pybuiltins.TypeError : PyNULL
 
 struct pyjlany_rev_op{OP}
-    op :: OP
+    op::OP
 end
 function (op::pyjlany_rev_op)(self, other_::Py)
     if pyisjl(other_)
@@ -224,7 +224,7 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py)
     for m in mimes
         try
             io = IOBuffer()
-            show(IOContext(io, :limit=>true), MIME(m), self)
+            show(IOContext(io, :limit => true), MIME(m), self)
             v = take!(io)
             ans[m] = vo = istextmime(m) ? pystr(String(v)) : pybytes(v)
             pydel!(vo)
@@ -278,7 +278,7 @@ 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))
+    pyjl(round(self; digits=ndigits))
 end
 pyjl_handle_error_type(::typeof(pyjlany_round), self, exc::MethodError) = pybuiltins.TypeError
 
diff --git a/test/Compat.jl b/test/Compat.jl
index 77deb310..a930a564 100644
--- a/test/Compat.jl
+++ b/test/Compat.jl
@@ -45,7 +45,7 @@ end
 @testitem "Serialization.jl" begin
     using Serialization
     @testset "Py" begin
-        for x in Py[Py(123), Py(1.23), Py("hello"), pylist([1, 2, 3]), pytuple([1, 2, 3]), Py(nothing), Py([1, 2, 3]), Py(:hello)]
+        @testset for x in Py[Py(123), Py(1.23), Py("hello"), pylist([1, 2, 3]), pytuple([1, 2, 3]), Py(nothing), Py([1, 2, 3]), Py(:hello)]
             io = IOBuffer()
             serialize(io, x)
             seekstart(io)
diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index b6364ccd..72d54851 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -20,7 +20,7 @@
     Base.powermod(x::Foo, y::Foo, z::Foo) = "powermod($(x.value), $(y.value), $(z.value))"
     (x::Foo)(args...; kw...) = "$(x.value)($args)$(length(kw))"
     Base.getindex(x::Foo, idx...) = "$(x.value)[$idx]"
-    Base.setindex!(x::Foo, v, idx...) = (x.value = v+sum(idx); x)
+    Base.setindex!(x::Foo, v, idx...) = (x.value = v + sum(idx); x)
     Base.delete!(x::Foo, idx...) = (x.value = -sum(idx); x)
     Base.in(v::Int, x::Foo) = x.value == v
     Base.nameof(x::Foo) = "nameof $(x.value)"
@@ -30,10 +30,12 @@
         @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"
@@ -105,7 +107,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 +115,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 +123,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 +131,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 +139,7 @@
         @test pyconvert(String, z) == "1 ÷ 2"
     end
     @testset "rfloordiv" begin
-        z = pyjlraw(Foo(1)) ÷ pyjl(Foo(2))
+        z = pyjl(Foo(2)).__rfloordiv__(pyjl(Foo(1)))
         @test pyconvert(String, z) == "1 ÷ 2"
     end
     @testset "mod" begin
@@ -145,15 +147,15 @@
         @test pyconvert(String, z) == "1 % 2"
     end
     @testset "rmod" begin
-        z = pyjlraw(Foo(1)) % pyjl(Foo(2))
+        z = pyjl(Foo(2)).__rmod__(pyjl(Foo(1)))
         @test pyconvert(String, z) == "1 % 2"
     end
     @testset "pow" begin
-        z = pyjl(Foo(1)) ^ pyjl(Foo(2))
+        z = pyjl(Foo(1))^pyjl(Foo(2))
         @test pyconvert(String, z) == "1 ^ 2"
     end
     @testset "rpow" begin
-        z = pyjlraw(Foo(1)) ^ pyjl(Foo(2))
+        z = pyjl(Foo(2)).__rpow__(pyjl(Foo(1)))
         @test pyconvert(String, z) == "1 ^ 2"
     end
     @testset "lshift" begin
@@ -161,7 +163,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 +171,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 +179,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 +187,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,12 +208,12 @@
         @test pycontains(z, "text/plain")
     end
     @testset "display" begin
-        pyjl(Foo(1))._jl_display()
-        pyjl(Foo(1))._jl_display(mime="text/plain")
+        pyjl(Foo(1)).jl_display()
+        pyjl(Foo(1)).jl_display(mime="text/plain")
     end
     @testset "help" begin
-        pyjl(Foo(1))._jl_help()
-        pyjl(Foo(1))._jl_help(mime="text/plain")
+        pyjl(Foo(1)).jl_help()
+        pyjl(Foo(1)).jl_help(mime="text/plain")
     end
 end
 
@@ -247,16 +249,16 @@ end
         @test pyeq(Bool, x[-1], 5)
         @test pyeq(Bool, x[-2], 4)
         @test pyeq(Bool, x[-3], 3)
-        @test pyjlvalue(x[pyslice(3)]) == [1,2,3]
-        @test pyjlvalue(x[pyslice(2)]) == [1,2]
-        @test pyjlvalue(x[pyslice(1,2)]) == [2]
-        @test pyjlvalue(x[pyslice(2,2)]) == []
-        @test pyjlvalue(x[pyslice(0,-1)]) == [1,2,3,4]
-        @test pyjlvalue(x[pyslice(-2,nothing)]) == [4,5]
-        @test pyjlvalue(x[pyslice(0, 3, 1)]) == [1,2,3]
-        @test pyjlvalue(x[pyslice(nothing,nothing,2)]) == [1,3,5]
-        @test pyjlvalue(x[pyslice(1,nothing,2)]) == [2,4]
-        @test pyjlvalue(x[pyslice(0,nothing,3)]) == [1,4]
+        @test pyjlvalue(x[pyslice(3)]) == [1, 2, 3]
+        @test pyjlvalue(x[pyslice(2)]) == [1, 2]
+        @test pyjlvalue(x[pyslice(1, 2)]) == [2]
+        @test pyjlvalue(x[pyslice(2, 2)]) == []
+        @test pyjlvalue(x[pyslice(0, -1)]) == [1, 2, 3, 4]
+        @test pyjlvalue(x[pyslice(-2, nothing)]) == [4, 5]
+        @test pyjlvalue(x[pyslice(0, 3, 1)]) == [1, 2, 3]
+        @test pyjlvalue(x[pyslice(nothing, nothing, 2)]) == [1, 3, 5]
+        @test pyjlvalue(x[pyslice(1, nothing, 2)]) == [2, 4]
+        @test pyjlvalue(x[pyslice(0, nothing, 3)]) == [1, 4]
         x = pyjl([1 2; 3 4])
         @test pyeq(Bool, x[0, 0], 1)
         @test pyeq(Bool, x[0, 1], 2)
@@ -268,19 +270,19 @@ end
     @testset "setitem" begin
         x = [0 0; 0 0]
         y = pyjl(x)
-        y[0,0] = 1
+        y[0, 0] = 1
         @test x == [1 0; 0 0]
-        y[0,1] = 2
+        y[0, 1] = 2
         @test x == [1 2; 0 0]
-        y[1,0] = 3
+        y[1, 0] = 3
         @test x == [1 2; 3 0]
-        y[-1,0] = 4
+        y[-1, 0] = 4
         @test x == [1 2; 4 0]
-        y[-2,pyslice(nothing)] = 5
+        y[-2, pyslice(nothing)] = 5
         @test x == [5 5; 4 0]
-        y[pyslice(nothing),-1] = 6
+        y[pyslice(nothing), -1] = 6
         @test x == [5 6; 4 6]
-        y[pyslice(nothing),pyslice(nothing)] = 7
+        y[pyslice(nothing), pyslice(nothing)] = 7
         @test x == [7 7; 7 7]
     end
     @testset "delitem" begin
@@ -292,7 +294,7 @@ end
         @test x == [2, 3, 5, 6, 7, 8]
         pydelitem(y, -3)
         @test x == [2, 3, 5, 7, 8]
-        pydelitem(y, pyslice(1,nothing,2))
+        pydelitem(y, pyslice(1, nothing, 2))
         @test x == [2, 5, 8]
     end
     @testset "reshape" begin
@@ -309,7 +311,7 @@ end
         @test pyjlvalue(x) == pyjlvalue(y)
         @test typeof(pyjlvalue(x)) == typeof(pyjlvalue(y))
         @test pyjlvalue(x) !== pyjlvalue(y)
-        x[0,0] = 0
+        x[0, 0] = 0
         @test pyjlvalue(x) == [0 2; 3 4]
         @test pyjlvalue(y) == [1 2; 3 4]
     end
@@ -334,7 +336,7 @@ end
         @test pytruth(m.f_contiguous)
         @test pyeq(Bool, m.format, "f")
         @test pyeq(Bool, m.itemsize, 4)
-        @test pyeq(Bool, m.nbytes, 4*6)
+        @test pyeq(Bool, m.nbytes, 4 * 6)
         @test pyeq(Bool, m.ndim, 2)
         @test !pytruth(m.readonly)
         @test pyeq(Bool, m.shape, (2, 3))
@@ -358,7 +360,7 @@ end
     end
     @testset "bool" begin
         @test !pytruth(pyjl(Dict()))
-        @test pytruth(pyjl(Dict("one"=>1, "two"=>2)))
+        @test pytruth(pyjl(Dict("one" => 1, "two" => 2)))
     end
 end
 
@@ -375,7 +377,7 @@ end
 end
 
 @testitem "iter" begin
-    x1 = [1,2,3,4,5]
+    x1 = [1, 2, 3, 4, 5]
     x2 = pyjl(x1)
     x3 = pylist(x2)
     x4 = pyconvert(Vector{Int}, x3)
@@ -400,19 +402,19 @@ end
     @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 // 1)), PythonCall.pyjlrationaltype)
         @test pyis(pytype(pyjl(0.0)), PythonCall.pyjlrealtype)
         @test pyis(pytype(pyjl(Complex(0.0))), PythonCall.pyjlcomplextype)
     end
     @testset "bool" begin
         @test !pytruth(pyjl(false))
         @test !pytruth(pyjl(0))
-        @test !pytruth(pyjl(0//1))
+        @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(5 // 2))
         @test pytruth(pyjl(2.3))
         @test pytruth(pyjl(Complex(1.2, 3.4)))
     end
@@ -422,17 +424,13 @@ end
 
 end
 
-@testitem "raw" begin
-
-end
-
 @testitem "set" begin
     @testset "type" begin
         @test pyis(pytype(pyjl(Set())), PythonCall.pyjlsettype)
     end
     @testset "bool" begin
         @test !pytruth(pyjl(Set()))
-        @test pytruth(pyjl(Set([1,2,3])))
+        @test pytruth(pyjl(Set([1, 2, 3])))
     end
 end
 
@@ -452,7 +450,7 @@ end
     @testset "bool" begin
         @test !pytruth(pyjl([]))
         @test pytruth(pyjl([1]))
-        @test pytruth(pyjl([1,2]))
+        @test pytruth(pyjl([1, 2]))
     end
     @testset "resize" begin
         x = pyjl([1, 2, 3, 4, 5])
@@ -469,91 +467,91 @@ end
         @test pyjlvalue(x) == [5, 6]
     end
     @testset "sort" begin
-        x = pyjl([4,6,2,3,7,6,1])
+        x = pyjl([4, 6, 2, 3, 7, 6, 1])
         x.sort()
-        @test pyjlvalue(x) == [1,2,3,4,6,6,7]
-        x = pyjl([4,6,2,3,7,6,1])
+        @test pyjlvalue(x) == [1, 2, 3, 4, 6, 6, 7]
+        x = pyjl([4, 6, 2, 3, 7, 6, 1])
         x.sort(reverse=true)
-        @test pyjlvalue(x) == [7,6,6,4,3,2,1]
-        x = pyjl([4,-6,2,-3,7,-6,1])
+        @test pyjlvalue(x) == [7, 6, 6, 4, 3, 2, 1]
+        x = pyjl([4, -6, 2, -3, 7, -6, 1])
         x.sort(key=abs)
-        @test pyjlvalue(x) == [1,2,-3,4,-6,-6,7]
-        x = pyjl([4,-6,2,-3,7,-6,1])
+        @test pyjlvalue(x) == [1, 2, -3, 4, -6, -6, 7]
+        x = pyjl([4, -6, 2, -3, 7, -6, 1])
         x.sort(key=abs, reverse=true)
-        @test pyjlvalue(x) == [7,-6,-6,4,-3,2,1]
+        @test pyjlvalue(x) == [7, -6, -6, 4, -3, 2, 1]
     end
     @testset "reverse" begin
-        x = pyjl([1,2,3,4,5])
+        x = pyjl([1, 2, 3, 4, 5])
         x.reverse()
-        @test pyjlvalue(x) == [5,4,3,2,1]
+        @test pyjlvalue(x) == [5, 4, 3, 2, 1]
     end
     @testset "clear" begin
-        x = pyjl([1,2,3,4,5])
-        @test pyjlvalue(x) == [1,2,3,4,5]
+        x = pyjl([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 = pyjl([1, 2, 3, 4, 5])
         y = pybuiltins.reversed(x)
-        @test pyjlvalue(x) == [1,2,3,4,5]
-        @test pyjlvalue(y) == [5,4,3,2,1]
+        @test pyjlvalue(x) == [1, 2, 3, 4, 5]
+        @test pyjlvalue(y) == [5, 4, 3, 2, 1]
     end
     @testset "insert" begin
-        x = pyjl([1,2,3])
+        x = pyjl([1, 2, 3])
         x.insert(0, 4)
-        @test pyjlvalue(x) == [4,1,2,3]
+        @test pyjlvalue(x) == [4, 1, 2, 3]
         x.insert(2, 5)
-        @test pyjlvalue(x) == [4,1,5,2,3]
+        @test pyjlvalue(x) == [4, 1, 5, 2, 3]
         x.insert(5, 6)
-        @test pyjlvalue(x) == [4,1,5,2,3,6]
+        @test pyjlvalue(x) == [4, 1, 5, 2, 3, 6]
         x.insert(-3, 7)
-        @test pyjlvalue(x) == [4,1,5,7,2,3,6]
+        @test pyjlvalue(x) == [4, 1, 5, 7, 2, 3, 6]
         @test_throws PyException x.insert(10, 10)
     end
     @testset "append" begin
-        x = pyjl([1,2,3])
+        x = pyjl([1, 2, 3])
         x.append(4)
-        @test pyjlvalue(x) == [1,2,3,4]
+        @test pyjlvalue(x) == [1, 2, 3, 4]
         x.append(5.0)
-        @test pyjlvalue(x) == [1,2,3,4,5]
+        @test pyjlvalue(x) == [1, 2, 3, 4, 5]
         @test_throws PyException x.append(nothing)
         @test_throws PyException x.append(1.2)
         @test_throws PyException x.append("2")
     end
     @testset "extend" begin
-        x = pyjl([1,2,3])
+        x = pyjl([1, 2, 3])
         x.extend(pylist())
-        @test pyjlvalue(x) == [1,2,3]
-        x.extend(pylist([4,5]))
-        @test pyjlvalue(x) == [1,2,3,4,5]
+        @test pyjlvalue(x) == [1, 2, 3]
+        x.extend(pylist([4, 5]))
+        @test pyjlvalue(x) == [1, 2, 3, 4, 5]
         x.extend(pylist([6.0]))
-        @test pyjlvalue(x) == [1,2,3,4,5,6]
+        @test pyjlvalue(x) == [1, 2, 3, 4, 5, 6]
     end
     @testset "pop" begin
-        x = pyjl([1,2,3,4,5])
+        x = pyjl([1, 2, 3, 4, 5])
         @test pyeq(Bool, x.pop(), 5)
-        @test pyjlvalue(x) == [1,2,3,4]
+        @test pyjlvalue(x) == [1, 2, 3, 4]
         @test pyeq(Bool, x.pop(0), 1)
-        @test pyjlvalue(x) == [2,3,4]
+        @test pyjlvalue(x) == [2, 3, 4]
         @test pyeq(Bool, x.pop(1), 3)
-        @test pyjlvalue(x) == [2,4]
+        @test pyjlvalue(x) == [2, 4]
         @test pyeq(Bool, x.pop(-2), 2)
         @test pyjlvalue(x) == [4]
         @test_throws PyException x.pop(10)
     end
     @testset "remove" begin
-        x = pyjl([1,3,2,4,5,3,1])
-        @test pyjlvalue(x) == [1,3,2,4,5,3,1]
+        x = pyjl([1, 3, 2, 4, 5, 3, 1])
+        @test pyjlvalue(x) == [1, 3, 2, 4, 5, 3, 1]
         x.remove(3)
-        @test pyjlvalue(x) == [1,2,4,5,3,1]
+        @test pyjlvalue(x) == [1, 2, 4, 5, 3, 1]
         @test_throws PyException x.remove(0)
         @test_throws PyException x.remove(nothing)
         @test_throws PyException x.remove("2")
-        @test pyjlvalue(x) == [1,2,4,5,3,1]
+        @test pyjlvalue(x) == [1, 2, 4, 5, 3, 1]
     end
     @testset "index" begin
-        x = pyjl([1,3,2,4,5,2,1])
+        x = pyjl([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 +564,7 @@ end
         @test_throws PyException x.index("2")
     end
     @testset "count" begin
-        x = pyjl([1,2,3,4,5,1,2,3,1])
+        x = pyjl([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)

From 4f67c769d87d9a14a5d822fa55b588fbab58a4fc Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:47:51 +0100
Subject: [PATCH 17/39] remove old tests

---
 test/JlWrap.jl | 53 ++++++++------------------------------------------
 1 file changed, 8 insertions(+), 45 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 72d54851..61db90db 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -54,6 +54,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)
@@ -215,6 +218,11 @@
         pyjl(Foo(1)).jl_help()
         pyjl(Foo(1)).jl_help(mime="text/plain")
     end
+    @testset "eval" begin
+        m = pyjl(Main)
+        @test pyconvert(Any, m.jl_eval("1 + 1")) === 2 # Basic behavior
+        @test pyconvert(Any, m.jl_eval("1 + 1\n ")) === 2 # Trailing whitespace
+    end
 end
 
 @testitem "array" begin
@@ -384,42 +392,6 @@ end
     @test x1 == x4
 end
 
-@testitem "module" begin
-    @testset "type" begin
-        @test pyis(pytype(pyjl(PythonCall)), PythonCall.pyjlmoduletype)
-    end
-    @testset "bool" begin
-        @test pytruth(pyjl(PythonCall))
-    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
-    end
-end
-
-@testitem "number" 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)
-    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)))
-    end
-end
-
 @testitem "objectarray" begin
 
 end
@@ -434,15 +406,6 @@ end
     end
 end
 
-@testitem "type" begin
-    @testset "type" begin
-        @test pyis(pytype(pyjl(Int)), PythonCall.pyjltypetype)
-    end
-    @testset "bool" begin
-        @test pytruth(pyjl(Int))
-    end
-end
-
 @testitem "vector" begin
     @testset "type" begin
         @test pyis(pytype(pyjl([1, 2, 3, 4])), PythonCall.pyjlvectortype)

From 157d725680cb25784aedd48ced47730e71dd6c6d Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:49:02 +0100
Subject: [PATCH 18/39] pyjlset tests

---
 test/JlWrap.jl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 61db90db..f124e544 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -398,11 +398,11 @@ 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
 

From c271815830f2d52e0426659d5c520f48ea05c540 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:49:47 +0100
Subject: [PATCH 19/39] pyjldict tests

---
 test/JlWrap.jl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index f124e544..a0f3b599 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -364,11 +364,11 @@ end
 
 @testitem "dict" begin
     @testset "type" begin
-        @test pyis(pytype(pyjl(Dict())), PythonCall.pyjldicttype)
+        @test pyis(pytype(pyjldict(Dict())), PythonCall.pyjldicttype)
     end
     @testset "bool" begin
-        @test !pytruth(pyjl(Dict()))
-        @test pytruth(pyjl(Dict("one" => 1, "two" => 2)))
+        @test !pytruth(pyjldict(Dict()))
+        @test pytruth(pyjldict(Dict("one" => 1, "two" => 2)))
     end
 end
 

From 5e79f89ccdef5f038bf5f06e10bf3c072ae67b31 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:50:32 +0100
Subject: [PATCH 20/39] pyjlio tests

---
 test/JlWrap.jl | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index a0f3b599..7c18759a 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -374,7 +374,6 @@ 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)
     end

From 6b1cdd2e19d462311a85cc952d7de0ec087528e4 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:54:44 +0100
Subject: [PATCH 21/39] pyjlarray tests

---
 test/JlWrap.jl | 47 ++++++++++++++++++++++++-----------------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 7c18759a..1fe928d8 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -227,30 +227,31 @@ 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)
@@ -267,7 +268,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)
@@ -277,7 +278,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
@@ -295,7 +296,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)
@@ -306,14 +307,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)
@@ -324,7 +325,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"], "<f4")
@@ -338,7 +339,7 @@ end
         # x = pyjl(Float32[1 2 3; 4 5 6]).__array_struct__
     end
     @testset "buffer" begin
-        m = pybuiltins.memoryview(pyjl(Float32[1 2 3; 4 5 6]))
+        m = pybuiltins.memoryview(pyjlarray(Float32[1 2 3; 4 5 6]))
         @test !pytruth(m.c_contiguous)
         @test pytruth(m.contiguous)
         @test pytruth(m.f_contiguous)

From 1ff9df67abd0485edb69bde3cf1ef2f863b3c150 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Mon, 10 Jun 2024 17:56:38 +0100
Subject: [PATCH 22/39] pyjlvector tests

---
 test/JlWrap.jl | 38 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 1fe928d8..82170311 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -408,15 +408,15 @@ 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]
@@ -430,38 +430,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)
@@ -473,7 +473,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)
@@ -483,7 +483,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]))
@@ -492,7 +492,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)
@@ -504,7 +504,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]
@@ -514,7 +514,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)
@@ -527,7 +527,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)

From 034f4872b3e205e432f99947234e089252733595 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Tue, 11 Jun 2024 17:46:26 +0100
Subject: [PATCH 23/39] pyjl hash, == and iter

---
 src/JlWrap/any.jl   |  24 +++++--
 src/JlWrap/array.jl | 160 ++++++++++++++++++++++----------------------
 src/JlWrap/dict.jl  |   2 +-
 src/JlWrap/io.jl    |   4 +-
 src/JlWrap/set.jl   |   2 -
 test/JlWrap.jl      |  23 +++++++
 6 files changed, 125 insertions(+), 90 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 3e21cde7..df42c124 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -332,6 +332,8 @@ end
 
 pyjlany_hash(self) = pyint(hash(self))
 
+pyjlmixin_eq_bool(self, other) = pybool((self == pyjlvalue(other))::Bool)
+
 function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
@@ -345,22 +347,30 @@ function init_any()
             else:
                 name = t.__name__
             return name + ":" + self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
-    class _JlHashMixin:
+    class JlIter(JlBase, _JlReprMixin):
         __slots__ = ()
-        def __hash__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyint ∘ hash)))
-    class JlIter(JlBase, _JlReprMixin, _JlHashMixin):
         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 _JlContainerMixin(_JlReprMixin, _JlHashMixin):
+    class _JlContainerMixin(_JlReprMixin):
         __slots__ = ()
         def __len__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyint ∘ length)))
         def __bool__(self):
             return self._jl_callmethod($(pyjl_methodnum(pybool ∘ !isempty)))
-    class Jl(JlBase, _JlReprMixin, _JlHashMixin):
+        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(pyjlmixin_eq_bool)), other)
+            else:
+                return NotImplemented
+    class Jl(JlBase, _JlReprMixin):
         __slots__ = ()
         def __str__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_str)))
@@ -464,6 +474,8 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_rev_op(⊻))), other)
         def __ror__(self, other):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_rev_op(|))), other)
+        def __hash__(self):
+            return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash)))
         def __eq__(self, other):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_op(==))), other)
         def __ne__(self, other):
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 4af140c8..d74f892d 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -63,7 +63,7 @@ function pyjl_getarrayindices(x::AbstractArray{T,N}, ks::Py) where {T,N}
     if pyistuple(ks)
         if pylen(ks) == N
             return ntuple(N) do i
-                k = pytuple_getitem(ks, i-1)
+                k = pytuple_getitem(ks, i - 1)
                 ans = pyjl_getaxisindex(axes(x, i), k)
                 pydel!(k)
                 return ans
@@ -149,12 +149,12 @@ pyjlarray_isbufferabletype(::Type{NamedTuple{names,T}}) where {names,T} =
 function pyjlarray_buffer_info(x::AbstractArray{T,N}) where {T,N}
     if pyjlarray_isbufferabletype(T)
         Cjl.PyBufferInfo{N}(
-            ptr = Base.unsafe_convert(Ptr{T}, x),
-            readonly = !Utils.ismutablearray(x),
-            itemsize = sizeof(T),
-            format = pybufferformat(T),
-            shape = size(x),
-            strides = strides(x) .* Base.aligned_sizeof(T),
+            ptr=Base.unsafe_convert(Ptr{T}, x),
+            readonly=!Utils.ismutablearray(x),
+            itemsize=sizeof(T),
+            format=pybufferformat(T),
+            shape=size(x),
+            strides=strides(x) .* Base.aligned_sizeof(T),
         )
     else
         error("element type is not bufferable")
@@ -163,43 +163,44 @@ end
 
 const PYBUFFERFORMAT = IdDict{Type,String}()
 
-pybufferformat(::Type{T}) where {T} = get!(PYBUFFERFORMAT, T) do
-    T == Cchar ? "b" :
-    T == Cuchar ? "B" :
-    T == Cshort ? "h" :
-    T == Cushort ? "H" :
-    T == Cint ? "i" :
-    T == Cuint ? "I" :
-    T == Clong ? "l" :
-    T == Culong ? "L" :
-    T == Clonglong ? "q" :
-    T == Culonglong ? "Q" :
-    T == Float16 ? "e" :
-    T == Cfloat ? "f" :
-    T == Cdouble ? "d" :
-    T == Complex{Float16} ? "Ze" :
-    T == Complex{Cfloat} ? "Zf" :
-    T == Complex{Cdouble} ? "Zd" :
-    T == Bool ? "?" :
-    T == Ptr{Cvoid} ? "P" :
-    if isstructtype(T) && isconcretetype(T) && allocatedinline(T)
-        n = fieldcount(T)
-        flds = []
-        for i = 1:n
-            nm = fieldname(T, i)
-            tp = fieldtype(T, i)
-            push!(flds, string(pybufferformat(tp), nm isa Symbol ? ":$nm:" : ""))
-            d =
-                (i == n ? sizeof(T) : fieldoffset(T, i + 1)) -
-                (fieldoffset(T, i) + sizeof(tp))
-            @assert d ≥ 0
-            d > 0 && push!(flds, "$(d)x")
+pybufferformat(::Type{T}) where {T} =
+    get!(PYBUFFERFORMAT, T) do
+        T == Cchar ? "b" :
+        T == Cuchar ? "B" :
+        T == Cshort ? "h" :
+        T == Cushort ? "H" :
+        T == Cint ? "i" :
+        T == Cuint ? "I" :
+        T == Clong ? "l" :
+        T == Culong ? "L" :
+        T == Clonglong ? "q" :
+        T == Culonglong ? "Q" :
+        T == Float16 ? "e" :
+        T == Cfloat ? "f" :
+        T == Cdouble ? "d" :
+        T == Complex{Float16} ? "Ze" :
+        T == Complex{Cfloat} ? "Zf" :
+        T == Complex{Cdouble} ? "Zd" :
+        T == Bool ? "?" :
+        T == Ptr{Cvoid} ? "P" :
+        if isstructtype(T) && isconcretetype(T) && allocatedinline(T)
+            n = fieldcount(T)
+            flds = []
+            for i = 1:n
+                nm = fieldname(T, i)
+                tp = fieldtype(T, i)
+                push!(flds, string(pybufferformat(tp), nm isa Symbol ? ":$nm:" : ""))
+                d =
+                    (i == n ? sizeof(T) : fieldoffset(T, i + 1)) -
+                    (fieldoffset(T, i) + sizeof(tp))
+                @assert d ≥ 0
+                d > 0 && push!(flds, "$(d)x")
+            end
+            string("T{", join(flds, " "), "}")
+        else
+            "$(sizeof(T))x"
         end
-        string("T{", join(flds, " "), "}")
-    else
-        "$(sizeof(T))x"
     end
-end
 
 pyjlarray_isarrayabletype(::Type{T}) where {T} = T in (
     UInt8,
@@ -227,44 +228,45 @@ pyjlarray_isarrayabletype(::Type{NamedTuple{names,types}}) where {names,types} =
 
 const PYTYPESTRDESCR = IdDict{Type,Tuple{String,Py}}()
 
-pytypestrdescr(::Type{T}) where {T} = get!(PYTYPESTRDESCR, T) do
-    c = Utils.islittleendian() ? '<' : '>'
-    T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) :
-    T == Int8 ? ("$(c)i1", PyNULL) :
-    T == UInt8 ? ("$(c)u1", PyNULL) :
-    T == Int16 ? ("$(c)i2", PyNULL) :
-    T == UInt16 ? ("$(c)u2", PyNULL) :
-    T == Int32 ? ("$(c)i4", PyNULL) :
-    T == UInt32 ? ("$(c)u4", PyNULL) :
-    T == Int64 ? ("$(c)i8", PyNULL) :
-    T == UInt64 ? ("$(c)u8", PyNULL) :
-    T == Float16 ? ("$(c)f2", PyNULL) :
-    T == Float32 ? ("$(c)f4", PyNULL) :
-    T == Float64 ? ("$(c)f8", PyNULL) :
-    T == Complex{Float16} ? ("$(c)c4", PyNULL) :
-    T == Complex{Float32} ? ("$(c)c8", PyNULL) :
-    T == Complex{Float64} ? ("$(c)c16", PyNULL) :
-    if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
-        n = fieldcount(T)
-        flds = []
-        for i = 1:n
-            nm = fieldname(T, i)
-            tp = fieldtype(T, i)
-            ts, ds = pytypestrdescr(tp)
-            isempty(ts) && return ("", PyNULL)
-            push!(
-                flds,
-                (nm isa Integer ? "f$(nm-1)" : string(nm), pyisnull(ds) ? ts : ds),
-            )
-            d = (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - (fieldoffset(T, i) + sizeof(tp))
-            @assert d ≥ 0
-            d > 0 && push!(flds, ("", "|V$(d)"))
+pytypestrdescr(::Type{T}) where {T} =
+    get!(PYTYPESTRDESCR, T) do
+        c = Utils.islittleendian() ? '<' : '>'
+        T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) :
+        T == Int8 ? ("$(c)i1", PyNULL) :
+        T == UInt8 ? ("$(c)u1", PyNULL) :
+        T == Int16 ? ("$(c)i2", PyNULL) :
+        T == UInt16 ? ("$(c)u2", PyNULL) :
+        T == Int32 ? ("$(c)i4", PyNULL) :
+        T == UInt32 ? ("$(c)u4", PyNULL) :
+        T == Int64 ? ("$(c)i8", PyNULL) :
+        T == UInt64 ? ("$(c)u8", PyNULL) :
+        T == Float16 ? ("$(c)f2", PyNULL) :
+        T == Float32 ? ("$(c)f4", PyNULL) :
+        T == Float64 ? ("$(c)f8", PyNULL) :
+        T == Complex{Float16} ? ("$(c)c4", PyNULL) :
+        T == Complex{Float32} ? ("$(c)c8", PyNULL) :
+        T == Complex{Float64} ? ("$(c)c16", PyNULL) :
+        if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
+            n = fieldcount(T)
+            flds = []
+            for i = 1:n
+                nm = fieldname(T, i)
+                tp = fieldtype(T, i)
+                ts, ds = pytypestrdescr(tp)
+                isempty(ts) && return ("", PyNULL)
+                push!(
+                    flds,
+                    (nm isa Integer ? "f$(nm-1)" : string(nm), pyisnull(ds) ? ts : ds),
+                )
+                d = (i == n ? sizeof(T) : fieldoffset(T, i + 1)) - (fieldoffset(T, i) + sizeof(tp))
+                @assert d ≥ 0
+                d > 0 && push!(flds, ("", "|V$(d)"))
+            end
+            ("|$(sizeof(T))V", pylist(flds))
+        else
+            ("", PyNULL)
         end
-        ("|$(sizeof(T))V", pylist(flds))
-    else
-        ("", PyNULL)
     end
-end
 
 pyjlarray_array__array(x::AbstractArray) = x isa Array ? Py(nothing) : pyjlarray(Array(x))
 pyjlarray_array__pyobjectarray(x::AbstractArray) = pyjlarray(PyObjectArray(x))
@@ -316,8 +318,6 @@ function init_array()
             self._jl_callmethod($(pyjl_methodnum(pyjlarray_setitem)), k, v)
         def __delitem__(self, k):
             self._jl_callmethod($(pyjl_methodnum(pyjlarray_delitem)), k)
-        def __iter__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator)))
         @property
         def __array_interface__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlarray_array_interface)))
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index 66586ddf..bcedc9b4 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -21,7 +21,7 @@ pyjldict_delitem(x::AbstractDict, k::Py) = (delete!(x, pyconvert(keytype(x), k))
 
 function pyjldict_update(x::AbstractDict, items_::Py)
     for item_ in items_
-        (k, v) = pyconvert(Tuple{keytype(x), valtype(x)}, item_)
+        (k, v) = pyconvert(Tuple{keytype(x),valtype(x)}, item_)
         x[k] = v
     end
     Py(nothing)
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index a0d77afb..fb8cd9d8 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -205,10 +205,12 @@ function init_io()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlIOBase(JlBase, _JlReprMixin, _JlHashMixin):
+    class JlIOBase(JlBase, _JlReprMixin):
         __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
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index a3e7b805..2a3a420c 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -81,8 +81,6 @@ function init_set()
         __slots__ = ()
         def __init__(self, value=None):
             JlBase.__init__(self, value, Base.AbstractSet)
-        def __iter__(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjliter ∘ Iterator)))
         def add(self, value):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_add)), value)
         def discard(self, value):
diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 82170311..1b62df40 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -353,6 +353,29 @@ end
         @test pyeq(Bool, m.suboffsets, ())
         @test pyeq(Bool, m.tolist(), pylist([pylist([1, 2, 3]), pylist([4, 5, 6])]))
     end
+    @testset "iter" begin
+        @test pyeq(Bool, pybuiltins.list(pyjlarray(fill(0))), pylist([0]))
+        @test pyeq(Bool, pybuiltins.list(pyjlarray([1, 2, 3])), pylist([1, 2, 3]))
+        @test pyeq(Bool, pybuiltins.list(pyjlarray([1 2; 3 4])), pylist([1, 3, 2, 4]))
+    end
+    @testset "eq" begin
+        @test pyeq(Bool, pyjlarray([1, 2, 3]), pyjlarray([1, 2, 3]))
+        @test pyeq(Bool, pyjlarray([1, 2, 3]), pyjlarray(1:3))
+        @test !pyeq(Bool, pyjlarray([1, 2, 3]), pyjlarray([2, 3, 4]))
+        @test !pyeq(Bool, pyjlarray([1, 2, 3]), pylist([1, 2, 3]))
+    end
+    @testset "hash" begin
+        @test pyhash(pyjlarray([1, 2, 3])) == pyhash(pyjlarray([1, 2, 3]))
+        @test pyhash(pyjlarray([1, 2, 3])) == pyhash(pyjlarray(1:3))
+        @test pyhash(pyjlarray([1, 2, 3])) != pyhash(pyjlarray([2, 3, 4]))
+        @test pyhash(pyjlarray([1, 2, 3])) != pyhash(pytuple([1, 2, 3]))
+    end
+    @testset "len" begin
+        @test pylen(pyjlarray([1, 2, 3])) == 3
+        @test pylen(pyjlarray([])) == 0
+        @test pylen(pyjlarray(fill(0))) == 1
+        @test pylen(pyjlarray([1 2; 3 4])) == 4
+    end
 end
 
 @testitem "base" begin

From 201fbf778310037404993b49a4432fa6c997abab Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Tue, 11 Jun 2024 18:00:29 +0100
Subject: [PATCH 24/39] add JlContainer

---
 src/JlWrap/JlWrap.jl     |  4 +++-
 src/JlWrap/any.jl        | 15 ---------------
 src/JlWrap/array.jl      |  2 +-
 src/JlWrap/collection.jl | 39 +++++++++++++++++++++++++++++++++++++++
 src/JlWrap/dict.jl       |  2 +-
 src/JlWrap/set.jl        |  2 +-
 6 files changed, 45 insertions(+), 19 deletions(-)
 create mode 100644 src/JlWrap/collection.jl

diff --git a/src/JlWrap/JlWrap.jl b/src/JlWrap/JlWrap.jl
index d79885ba..3f2e98b9 100644
--- a/src/JlWrap/JlWrap.jl
+++ b/src/JlWrap/JlWrap.jl
@@ -20,6 +20,7 @@ include("base.jl")
 include("any.jl")
 include("io.jl")
 include("objectarray.jl")
+include("collection.jl")
 include("array.jl")
 include("vector.jl")
 include("dict.jl")
@@ -27,10 +28,11 @@ include("set.jl")
 include("callback.jl")
 
 function __init__()
-    Cjl.C.with_gil() do 
+    Cjl.C.with_gil() do
         init_base()
         init_any()
         init_io()
+        init_collection()
         init_array()
         init_vector()
         init_dict()
diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index df42c124..0160f7c1 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -355,21 +355,6 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash)))
         def __next__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjliter_next)))
-    class _JlContainerMixin(_JlReprMixin):
-        __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(pyjlmixin_eq_bool)), other)
-            else:
-                return NotImplemented
     class Jl(JlBase, _JlReprMixin):
         __slots__ = ()
         def __str__(self):
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index d74f892d..3f892d59 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -297,7 +297,7 @@ function init_array()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlArray(JlBase, _JlContainerMixin):
+    class JlArray(JlCollection):
         __slots__ = ()
         _jl_buffer_info = $(pyjl_methodnum(pyjlarray_buffer_info))
         def __init__(self, value):
diff --git a/src/JlWrap/collection.jl b/src/JlWrap/collection.jl
new file mode 100644
index 00000000..31220f2c
--- /dev/null
+++ b/src/JlWrap/collection.jl
@@ -0,0 +1,39 @@
+const pyjlcollectiontype = pynew()
+
+function init_collection()
+    jl = pyjuliacallmodule
+    pybuiltins.exec(pybuiltins.compile("""
+    $("\n"^(@__LINE__()-1))
+    class JlCollection(JlBase, _JlReprMixin):
+        __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(pyjlmixin_eq_bool)), other)
+            else:
+                return NotImplemented
+        def copy(self):
+            return type(self)(Base.copy(self))
+    import collections.abc
+    collections.abc.Collection.register(JlCollection)
+    del collections
+    """, @__FILE__(), "exec"), jl.__dict__)
+    pycopy!(pyjlcollectiontype, jl.JlCollection)
+end
+
+"""
+    pyjlcollection(x::Union{AbstractArray,AbstractSet,AbstractDict})
+
+Wrap `x` as a Python `collections.abc.Collection` object.
+"""
+pyjlcollection(x::Union{AbstractArray,AbstractSet,AbstractDict}) = pyjl(pyjlcollectiontype, x)
+export pyjlcollection
+
+# Py(x::AbstractSet) = pyjlcollection(x)
diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index bcedc9b4..1763c53e 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -33,7 +33,7 @@ function init_dict()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlDict(JlBase, _JlContainerMixin):
+    class JlDict(JlCollection):
         __slots__ = ()
         _jl_undefined_ = object()
         def __init__(self, value=None):
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index 2a3a420c..bbb54ad9 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -77,7 +77,7 @@ function init_set()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlSet(JlBase, _JlContainerMixin):
+    class JlSet(JlCollection):
         __slots__ = ()
         def __init__(self, value=None):
             JlBase.__init__(self, value, Base.AbstractSet)

From 8391bd69b8464446d70cdc77019b156a56acdc20 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Tue, 11 Jun 2024 18:29:53 +0100
Subject: [PATCH 25/39] eliminate mixins and add contains and clear to
 JlCollection

---
 src/JlWrap/any.jl        |  8 +++-----
 src/JlWrap/array.jl      |  2 --
 src/JlWrap/collection.jl | 30 ++++++++++++++++++++++++------
 src/JlWrap/dict.jl       |  6 ------
 src/JlWrap/io.jl         |  2 +-
 src/JlWrap/set.jl        |  6 ------
 src/JlWrap/vector.jl     | 11 ++---------
 7 files changed, 30 insertions(+), 35 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 0160f7c1..476aee65 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -332,13 +332,11 @@ end
 
 pyjlany_hash(self) = pyint(hash(self))
 
-pyjlmixin_eq_bool(self, other) = pybool((self == pyjlvalue(other))::Bool)
-
 function init_any()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class _JlReprMixin:
+    class JlBase2(JlBase):
         __slots__ = ()
         def __repr__(self):
             t = type(self)
@@ -347,7 +345,7 @@ function init_any()
             else:
                 name = t.__name__
             return name + ":" + self._jl_callmethod($(pyjl_methodnum(pyjlany_repr)))
-    class JlIter(JlBase, _JlReprMixin):
+    class JlIter(JlBase2):
         __slots__ = ()
         def __iter__(self):
             return self
@@ -355,7 +353,7 @@ function init_any()
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_hash)))
         def __next__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjliter_next)))
-    class Jl(JlBase, _JlReprMixin):
+    class Jl(JlBase2):
         __slots__ = ()
         def __str__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlany_str)))
diff --git a/src/JlWrap/array.jl b/src/JlWrap/array.jl
index 3f892d59..fa52560b 100644
--- a/src/JlWrap/array.jl
+++ b/src/JlWrap/array.jl
@@ -308,8 +308,6 @@ function init_array()
         @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 __getitem__(self, k):
diff --git a/src/JlWrap/collection.jl b/src/JlWrap/collection.jl
index 31220f2c..d740597e 100644
--- a/src/JlWrap/collection.jl
+++ b/src/JlWrap/collection.jl
@@ -1,10 +1,16 @@
 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(JlBase, _JlReprMixin):
+    class JlCollection(JlBase2):
         __slots__ = ()
         def __len__(self):
             return self._jl_callmethod($(pyjl_methodnum(pyint ∘ length)))
@@ -16,11 +22,15 @@ function init_collection()
             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(pyjlmixin_eq_bool)), other)
+                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 type(self)(Base.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
@@ -29,11 +39,19 @@ function init_collection()
 end
 
 """
-    pyjlcollection(x::Union{AbstractArray,AbstractSet,AbstractDict})
+    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::Union{AbstractArray,AbstractSet,AbstractDict}) = pyjl(pyjlcollectiontype, x)
+pyjlcollection(x) = pyjl(pyjlcollectiontype, x)
+pyjlcollection(x::AbstractSet) = pyjlset(x)
+pyjlcollection(x::AbstractArray) = pyjlarray(x)
+pyjlcollection(x::AbstractDict) = pyjldict(x)
 export pyjlcollection
 
-# Py(x::AbstractSet) = pyjlcollection(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 1763c53e..154764ef 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -11,8 +11,6 @@ Base.in(v::Tuple{Any,Any}, x::DictPairSet) = Pair(v[1], v[2]) in x.dict
 
 pyjldict_contains(x::AbstractDict, k::Py) = Py(haskey(x, @pyconvert(keytype(x), k, return Py(false))))
 
-pyjldict_clear(x::AbstractDict) = (empty!(x); Py(nothing))
-
 pyjldict_getitem(x::AbstractDict, k::Py) = Py(x[pyconvert(keytype(x), k)])
 
 pyjldict_setitem(x::AbstractDict, k::Py, v::Py) = (x[pyconvertarg(keytype(x), k, "key")] = pyconvertarg(valtype(x), v, "value"); Py(nothing))
@@ -71,8 +69,6 @@ function init_dict()
             if key not in self:
                 self[key] = default
             return self[key]
-        def clear(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyjldict_clear)))
         def pop(self, key, default=_jl_undefined_):
             if key in self:
                 ans = self[key]
@@ -98,8 +94,6 @@ function init_dict()
                 self._jl_callmethod($(pyjl_methodnum(pyjldict_update)), items)
             if kwargs:
                 self.update(kwargs)
-        def copy(self):
-            return self._jl_callmethod($(pyjl_methodnum(Py ∘ copy)))
     import collections.abc
     collections.abc.MutableMapping.register(JlDict)
     del collections
diff --git a/src/JlWrap/io.jl b/src/JlWrap/io.jl
index fb8cd9d8..a1662f22 100644
--- a/src/JlWrap/io.jl
+++ b/src/JlWrap/io.jl
@@ -205,7 +205,7 @@ function init_io()
     jl = pyjuliacallmodule
     pybuiltins.exec(pybuiltins.compile("""
     $("\n"^(@__LINE__()-1))
-    class JlIOBase(JlBase, _JlReprMixin):
+    class JlIOBase(JlBase2):
         __slots__ = ()
         def __init__(self, value):
             JlBase.__init__(self, value, Base.IO)
diff --git a/src/JlWrap/set.jl b/src/JlWrap/set.jl
index bbb54ad9..47caa9c7 100644
--- a/src/JlWrap/set.jl
+++ b/src/JlWrap/set.jl
@@ -8,8 +8,6 @@ function pyjlset_discard(x::AbstractSet, v_::Py)
     Py(nothing)
 end
 
-pyjlset_clear(x::AbstractSet) = (empty!(x); Py(nothing))
-
 function pyjlset_pop(x::AbstractSet)
     if isempty(x)
         errset(pybuiltins.KeyError, "pop from an empty set")
@@ -85,10 +83,6 @@ function init_set()
             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(pyjlset ∘ copy)))
         def pop(self):
             return self._jl_callmethod($(pyjl_methodnum(pyjlset_pop)))
         def remove(self, value):
diff --git a/src/JlWrap/vector.jl b/src/JlWrap/vector.jl
index b78bd0e0..7b5074a7 100644
--- a/src/JlWrap/vector.jl
+++ b/src/JlWrap/vector.jl
@@ -25,11 +25,6 @@ function pyjlvector_reverse(x::AbstractVector)
     Py(nothing)
 end
 
-function pyjlvector_clear(x::AbstractVector)
-    empty!(x)
-    Py(nothing)
-end
-
 function pyjlvector_reversed(x::AbstractVector)
     Py(reverse(x))
 end
@@ -39,12 +34,12 @@ function pyjlvector_insert(x::AbstractVector, k_::Py, v_::Py)
     pydel!(k_)
     a = axes(x, 1)
     k′ = k < 0 ? (last(a) + 1 + k) : (first(a) + k)
-    if checkbounds(Bool, x, k′) || k′ == last(a)+1
+    if checkbounds(Bool, x, k′) || k′ == last(a) + 1
         v = pyconvertarg(eltype(x), v_, "value")
         insert!(x, k′, v)
         return Py(nothing)
     else
-        errset(pybuiltins.IndexError, "array index out of bounds");
+        errset(pybuiltins.IndexError, "array index out of bounds")
         return PyNULL
     end
 end
@@ -133,8 +128,6 @@ function init_vector()
             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):

From 192d7d5009bb00d73d3df7299221dbff7fcab879 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Tue, 11 Jun 2024 18:36:04 +0100
Subject: [PATCH 26/39] fix keys, values, items

---
 src/JlWrap/dict.jl | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/JlWrap/dict.jl b/src/JlWrap/dict.jl
index 154764ef..b6c7abbb 100644
--- a/src/JlWrap/dict.jl
+++ b/src/JlWrap/dict.jl
@@ -55,11 +55,11 @@ function init_dict()
             else:
                 raise KeyError(key)
         def keys(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyset ∘ keys)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ keys)))
         def values(self):
-            return self._jl_callmethod($(pyjl_methodnum(Py ∘ values)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlcollection ∘ values)))
         def items(self):
-            return self._jl_callmethod($(pyjl_methodnum(pyset ∘ DictPairSet)))
+            return self._jl_callmethod($(pyjl_methodnum(pyjlset ∘ DictPairSet)))
         def get(self, key, default=None):
             if key in self:
                 return self[key]

From 6ee17e18d464855ab06d1549777ece2422bc1957 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Tue, 11 Jun 2024 19:06:47 +0100
Subject: [PATCH 27/39] fix tests

---
 test/PyMacro.jl | 48 ++++++++++++++++++++++++++++++++++++------------
 1 file changed, 36 insertions(+), 12 deletions(-)

diff --git a/test/PyMacro.jl b/test/PyMacro.jl
index d3a1b1b0..cb8fc848 100644
--- a/test/PyMacro.jl
+++ b/test/PyMacro.jl
@@ -84,11 +84,11 @@
         @test x isa Py
         @test pyis(pytype(x), pybuiltins.dict)
         @test pyeq(Bool, x, pydict())
-        x = @py {x=1, y=2}
+        x = @py {x = 1, y = 2}
         @test x isa Py
         @test pyis(pytype(x), pybuiltins.dict)
         @test pyeq(Bool, x, pydict(x=1, y=2))
-        x = @py {"x": 1, "y": 2}
+        x = @py {"x":1, "y":2}
         @test x isa Py
         @test pyis(pytype(x), pybuiltins.dict)
         @test pyeq(Bool, x, pydict(x=1, y=2))
@@ -197,35 +197,59 @@
         @test pyis(_ver, sys.version_info)
     end
     @testset "short-circuit" begin
-        x = @py 3 && pylist([1,2])
+        x = @py 3 && @jl(pylist([1, 2]))
         @test pyeq(Bool, x, pylist([1, 2]))
         x = @py None && True
         @test pyis(x, pybuiltins.None)
-        x = @py None || 0 || pyset()
+        x = @py None || 0 || @jl(pyset())
         @test pyeq(Bool, x, pyset())
-        x = @py pydict() || 8 || ""
+        x = @py @jl(pydict()) || 8 || ""
         @test pyeq(Bool, x, 8)
     end
     @testset "if" begin
-        x = @py if 1 == 2; "a"; end
+        x = @py if 1 == 2
+            "a"
+        end
         @test x isa Py
         @test pyis(x, pybuiltins.None)
-        x = @py if 1 < 2; "a"; end
+        x = @py if 1 < 2
+            "a"
+        end
         @test x isa Py
         @test pyeq(Bool, x, "a")
-        x = @py if 1 == 2; "a"; else; "b"; end
+        x = @py if 1 == 2
+            "a"
+        else
+            "b"
+        end
         @test x isa Py
         @test pyeq(Bool, x, "b")
-        x = @py if 1 < 2; "a"; else; "b"; end
+        x = @py if 1 < 2
+            "a"
+        else
+            "b"
+        end
         @test x isa Py
         @test pyeq(Bool, x, "a")
-        x = @py if 1 == 2; "a"; elseif 1 < 2; "b"; end
+        x = @py if 1 == 2
+            "a"
+        elseif 1 < 2
+            "b"
+        end
         @test x isa Py
         @test pyeq(Bool, x, "b")
-        x = @py if 1 < 2; "a"; elseif 2 < 3; "b"; end
+        x = @py if 1 < 2
+            "a"
+        elseif 2 < 3
+            "b"
+        end
         @test x isa Py
         @test pyeq(Bool, x, "a")
-        x = @py if 1 == 2; "a"; elseif 2 == 3; "b"; end
+        x = @py if 1 == 2
+            "a"
+        elseif 2 == 3
+            "b"
+        end
         @test x isa Py
         @test pyis(x, pybuiltins.None)
     end

From a76e3d183424e0cc54196398ba6ff1247bdc3024 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 12 Jun 2024 10:14:12 +0100
Subject: [PATCH 28/39] tests for JlCollection

---
 test/JlWrap.jl | 128 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 128 insertions(+)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 1b62df40..da0bb661 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -225,6 +225,134 @@
     end
 end
 
+@testitem "collection" begin
+    cases = [
+        (
+            x=fill(nothing),
+            type=:array,
+            list=pylist([nothing]),
+        ),
+        (
+            x=[1, 2, 3],
+            type=:vector,
+            list=pylist([1, 2, 3]),
+        ),
+        (
+            x=[1 2; 3 4],
+            type=:array,
+            list=pylist([1, 3, 2, 4]),
+        ),
+        (
+            x=Set([1, 2, 3]),
+            type=:set,
+            list=pylist(Set([1, 2, 3])),
+        ),
+        (
+            x=Dict(1 => 2, 3 => 4),
+            type=:dict,
+            list=pylist(keys(Dict(1 => 2, 3 => 4))),
+        ),
+        (
+            x=keys(Dict()),
+            type=:set,
+            list=pylist(),
+        ),
+        (
+            x=values(Dict()),
+            type=:collection,
+            list=pylist(),
+        ),
+        (
+            x=(1, 2, 3),
+            type=:collection,
+            list=pylist([1, 2, 3]),
+        ),
+        (
+            x=(x=1, y=2),
+            type=:collection,
+            list=pylist([1, 2]),
+        ),
+        (
+            x=Ref(nothing),
+            type=:collection,
+            list=pylist([nothing]),
+        ),]
+    @testset "type $(c.x)" for c in cases
+        y = pyjlcollection(c.x)
+        @test pyisinstance(y, PythonCall.JlWrap.pyjlcollectiontype)
+        @test pyis(pytype(y), getproperty(PythonCall.JlWrap, Symbol(:pyjl, c.type, :type)))
+    end
+    @testset "len $(c.x)" for c in cases
+        @test pylen(pyjlcollection(c.x)) == length(c.x)
+    end
+    @testset "bool $(c.x)" for c in cases
+        @test pytruth(pyjlcollection(c.x)) == !isempty(c.x)
+    end
+    @testset "iter $(c.x)" for c in cases
+        @test pyeq(Bool, pylist(pyjlcollection(c.x)), c.list)
+    end
+    @testset "hash $(c.x)" for c in cases
+        # not sure why but the bottom byte doesn't always match
+        @test mod(pyhash(pyjlcollection(c.x)), UInt32) >> 8 == mod(hash(c.x), UInt32) >> 8
+    end
+    @testset "eq $(c1.x) $(c2.x)" for (c1, c2) in Iterators.product(cases, cases)
+        @test pyeq(Bool, pyjlcollection(c1.x), pyjlcollection(c2.x)) == (c1.x == c2.x)
+    end
+    @testset "contains $(c.x) $(v)" for (c, v) in Iterators.product(cases, [nothing, 0, 1, 2, 3, 4, 5, 0.0, 0.5, 1.0])
+        if !isa(c.x, Dict)
+            @test pycontains(pyjlcollection(c.x), v) == (v in c.x)
+        end
+    end
+    @testset "copy $(c.x)" for c in cases
+        copyable = try
+            copy(c.x)
+            true
+        catch
+            false
+        end
+        y = pyjlcollection(c.x)
+        if copyable
+            z = y.copy()
+            @test pyis(pytype(y), pytype(z))
+            yv = pyjlvalue(y)
+            zv = pyjlvalue(z)
+            @test yv === c.x
+            @test yv == zv
+            @test yv !== zv
+        else
+            @test_throws PyException y.copy()
+        end
+    end
+    @testset "clear $(c.x)" for c in cases
+        # make a copy or skip the test
+        x2 = try
+            copy(c.x)
+        catch
+            continue
+        end
+        len = length(x2)
+        # see if the collection can be emptied
+        clearable = try
+            empty!(copy(c.x))
+            true
+        catch
+            false
+        end
+        # try clearing the collection
+        y = pyjlcollection(x2)
+        @test pylen(y) == len
+        if clearable
+            y.clear()
+            @test pylen(y) == 0
+            @test length(x2) == 0
+        else
+            @test_throws PyException y.clear()
+            @test pylen(y) == len
+            @test length(x2) == len
+        end
+    end
+end
+
 @testitem "array" begin
     @testset "type" begin
         @test pyis(pytype(pyjlarray(fill(nothing))), PythonCall.pyjlarraytype)

From d9980313d1ac8753121863f25e2e827ef160167f Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 12 Jun 2024 10:43:51 +0100
Subject: [PATCH 29/39] update jlwrap docs

---
 docs/src/juliacall-reference.md | 47 ++++++++++++++++++++++-----------
 1 file changed, 32 insertions(+), 15 deletions(-)

diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md
index 916af967..52889e49 100644
--- a/docs/src/juliacall-reference.md
+++ b/docs/src/juliacall-reference.md
@@ -42,10 +42,11 @@ interface and behaves very similar to a list.
 
 - `JlBase`
   - [`Jl`](#juliacall.Jl)
-  - [`JlArray`](#juliacall.JlArray)
-    - [`JlVector`](#juliacall.JlVector)
-  - [`JlDict`](#juliacall.JlDict)
-  - [`JlSet`](#juliacall.JlSet)
+  - [`JlCollection`](#juliacall.JlCollection)
+    - [`JlArray`](#juliacall.JlArray)
+      - [`JlVector`](#juliacall.JlVector)
+    - [`JlDict`](#juliacall.JlDict)
+    - [`JlSet`](#juliacall.JlSet)
   - [`JlIOBase`](#juliacall.JlIOBase)
     - `JlBinaryIO`
     - `JlTextIO`
@@ -58,26 +59,44 @@ 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 `Jl`.
+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_callback(*args, **kwargs)`: Calls the Julia object with the given arguments.
+  Unlike ordinary calling syntax, the arguments are passed as `Py` objects instead of
+  being converted.
 - `jl_display()`: Display the object using Julia's display mechanism.
+- `jl_eval(expr)`: If the object is a Julia `Module`, evaluates the given expression.
 - `jl_help()`: Display help for the object.
+- `jl_to_py()`: Convert to a Python object using the [usual conversion rules](@ref jl2py).
+`````
+
+`````@customdoc
+juliacall.JlCollection - Class
+
+Wraps any Julia collection. It is a subclass of `collections.abc.Collection`.
+
+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!`.
+
+It supports `in`, `iter`, `len`, `hash`, `bool`, `==`.
+
+###### Members
+- `clear()`: Empty the collection in-place.
+- `copy()`: A copy of the collection.
 `````
 
 `````@customdoc
 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.
@@ -95,7 +114,6 @@ 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.
 `````
@@ -110,7 +128,6 @@ This wraps any Julia `AbstractVector` value. It is a subclass of `juliacall.JlAr
 - `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.

From 128071a3ec9af25aa9ecb5c0b1069e1c805a29e2 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 19 Jun 2024 16:48:36 +0100
Subject: [PATCH 30/39] Jl tests

---
 test/JlWrap.jl | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 71 insertions(+)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index da0bb661..053cff04 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -24,6 +24,7 @@
     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)
@@ -64,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((<py 4>, <py 5>))0"
+    end
     @testset "getitem" begin
         z = pygetitem(pyjl(Foo(1)), 3)
         @test pyconvert(String, z) == "1[(3,)]"
@@ -223,6 +228,72 @@
         @test pyconvert(Any, m.jl_eval("1 + 1")) === 2 # Basic behavior
         @test pyconvert(Any, m.jl_eval("1 + 1\n ")) === 2 # Trailing whitespace
     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 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

From 93b6ea80cb07fcca22e9548b8ba04c843be08695 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 19 Jun 2024 17:15:09 +0100
Subject: [PATCH 31/39] JlDict tests

---
 test/JlWrap.jl | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 116 insertions(+)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 053cff04..4c5855ac 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -593,6 +593,122 @@ end
         @test !pytruth(pyjldict(Dict()))
         @test pytruth(pyjldict(Dict("one" => 1, "two" => 2)))
     end
+    @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 "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 "io" begin

From 09c760c306895c4453331c62a1e1716f803ae65a Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 19 Jun 2024 17:30:42 +0100
Subject: [PATCH 32/39] PySet tests

---
 test/JlWrap.jl | 108 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 108 insertions(+)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index 4c5855ac..d4a97f3a 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -742,6 +742,114 @@ end
         @test !pytruth(pyjlset(Set()))
         @test pytruth(pyjlset(Set([1, 2, 3])))
     end
+    @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 "pop" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        zs = Py[]
+        for i in 1:3
+            push!(zs, y.pop())
+            @test length(x) == 3 - i
+        end
+        @test_throws PyException y.pop()
+        @test all(pyisinstance(z, pybuiltins.int) for z in zs)
+        @test pyeq(Bool, pyset(zs), pyset([1, 2, 3]))
+    end
+    @testset "remove" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        y.remove(1)
+        @test x == Set([2, 3])
+        @test_throws PyException y.remove(1)
+    end
+    @testset "difference" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.difference(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, pyset([2]))
+    end
+    @testset "intersection" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.intersection(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, pyset([1, 3]))
+    end
+    @testset "symmetric_difference" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.symmetric_difference(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, pyset([2, 5]))
+    end
+    @testset "union" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.union(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, pyset([1, 2, 3, 5]))
+    end
+    @testset "isdisjoint" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.isdisjoint(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, false)
+        z = y.isdisjoint(pyset([0, 5]))
+        @test pyeq(Bool, z, true)
+    end
+    @testset "issubset" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.issubset(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, false)
+        z = y.issubset(pyset([1, 2, 3, 4, 5]))
+        @test pyeq(Bool, z, true)
+    end
+    @testset "issuperset" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        z = y.issuperset(pyset([1, 3, 5]))
+        @test pyeq(Bool, z, false)
+        z = y.issuperset(pyset([1, 3]))
+        @test pyeq(Bool, z, true)
+    end
+    @testset "difference_update" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        y.difference_update(pyset([1, 3, 5]))
+        @test x == Set([2])
+    end
+    @testset "intersection_update" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        y.intersection_update(pyset([1, 3, 5]))
+        @test x == Set([1, 3])
+    end
+    @testset "symmetric_difference_update" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        y.symmetric_difference_update(pyset([1, 3, 5]))
+        @test x == Set([2, 5])
+    end
+    @testset "update" begin
+        x = Set([1, 2, 3])
+        y = pyjlset(x)
+        y.update(pyset([1, 3, 5]))
+        @test x == Set([1, 2, 3, 5])
+    end
 end
 
 @testitem "vector" begin

From 674ab2c0cc589f218ba141918a9cd71ec0f0226b Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Wed, 19 Jun 2024 18:08:58 +0100
Subject: [PATCH 33/39] JlIO tests

---
 test/JlWrap.jl | 258 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 256 insertions(+), 2 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index d4a97f3a..cbd95e1e 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -711,15 +711,269 @@ end
     end
 end
 
-@testitem "io" begin
+@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
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "flush" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "isatty $(typeof(x))" for (x, y) in [(IOBuffer(), false), (devnull, false), (stdout, false)]
+        # TODO: how to get a TTY in a test environment??
+        z = pytextio(x).isatty()
+        @test pyisinstance(z, pybuiltins.bool)
+        @test pyeq(Bool, z, y)
+    end
+    @testset "readable $(typeof(x))" for (x, y) in [(IOBuffer(), true), (devnull, false), (stdin, true)]
+        z = pytextio(x).readable()
+        @test pyisinstance(z, pybuiltins.bool)
+        @test pyeq(Bool, z, y)
+    end
+    @testset "readlines" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "seek" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "seekable $(typeof(x))" for (x, y) in [(IOBuffer(), true), (devnull, true), (stdin, true), (stdout, true)]
+        # TODO: currently always returns true, can this be improved??
+        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)
+        # TODO
+    end
+    @testset "truncate" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "writable" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "writelines" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "enter/exit" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "iter" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+    @testset "next" begin
+        x = IOBuffer()
+        print(x, "hello\n")
+        seekstart(x)
+        y = pytextio(x)
+        # TODO
+    end
+end
+
+@testitem "io/binary" begin
     @testset "type" begin
         @test pyis(pytype(pybinaryio(devnull)), PythonCall.pyjlbinaryiotype)
-        @test pyis(pytype(pytextio(devnull)), PythonCall.pyjltextiotype)
     end
     @testset "bool" begin
         @test pytruth(pybinaryio(devnull))
+    end
+    @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 "io/text" begin
+    @testset "type" begin
+        @test pyis(pytype(pytextio(devnull)), PythonCall.pyjltextiotype)
+    end
+    @testset "bool" begin
         @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 "iter" begin

From 77b5ee1b2707ac0717c209cc4a754d9c9b59219c Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 21:42:46 +0100
Subject: [PATCH 34/39] more JlIO tests

---
 test/JlWrap.jl | 100 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 80 insertions(+), 20 deletions(-)

diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index cbd95e1e..fb5ebc05 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -735,18 +735,27 @@ end
         @test pyeq(Bool, z, true)
     end
     @testset "fileno" begin
+        # IOBuffer has no fileno
         x = IOBuffer()
         print(x, "hello\n")
         seekstart(x)
         y = pytextio(x)
-        # TODO
+        @test_throws PyException y.fileno()
+        # check some file that has a fileno
+        mktemp() do name, x
+            y = pytextio(x)
+            z = y.fileno()
+            @test pyisinstance(z, pybuiltins.int)
+            @test pyeq(Bool, z, fd(x))
+        end
     end
     @testset "flush" begin
         x = IOBuffer()
         print(x, "hello\n")
         seekstart(x)
         y = pytextio(x)
-        # TODO
+        y.flush()
+        # TODO: check it actually flushed something
     end
     @testset "isatty $(typeof(x))" for (x, y) in [(IOBuffer(), false), (devnull, false), (stdout, false)]
         # TODO: how to get a TTY in a test environment??
@@ -762,19 +771,34 @@ end
     @testset "readlines" begin
         x = IOBuffer()
         print(x, "hello\n")
+        print(x, "world\n")
         seekstart(x)
         y = pytextio(x)
-        # TODO
+        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)
-        # TODO
+        @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)]
-        # TODO: currently always returns true, can this be improved??
         z = pytextio(x).seekable()
         @test pyisinstance(z, pybuiltins.bool)
         @test pyeq(Bool, z, y)
@@ -784,49 +808,85 @@ end
         print(x, "hello\n")
         seekstart(x)
         y = pytextio(x)
-        # TODO
+        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)
-        # TODO
-    end
-    @testset "writable" begin
-        x = IOBuffer()
-        print(x, "hello\n")
+        y.truncate(5)
+        seekend(x)
+        @test position(x) == 5
+        seek(x, 3)
+        y.truncate()
         seekstart(x)
-        y = pytextio(x)
-        # TODO
+        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()
-        print(x, "hello\n")
-        seekstart(x)
         y = pytextio(x)
-        # TODO
+        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()
-        print(x, "hello\n")
         seekstart(x)
         y = pytextio(x)
-        # TODO
+        @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)
-        # TODO
+        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)
-        # TODO
+        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
 

From e88de1e3c5f87146067861ffa55b1d8ae8f1e7a8 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 21:47:13 +0100
Subject: [PATCH 35/39] rename julia wrapper classes

---
 docs/src/conversion-to-python.md | 17 +++++++----------
 docs/src/juliacall-reference.md  |  2 +-
 pytest/test_all.py               |  2 +-
 3 files changed, 9 insertions(+), 12 deletions(-)

diff --git a/docs/src/conversion-to-python.md b/docs/src/conversion-to-python.md
index 8333d900..37c25d92 100644
--- a/docs/src/conversion-to-python.md
+++ b/docs/src/conversion-to-python.md
@@ -23,16 +23,13 @@ From Python, this occurs when converting the return value of a Julia function.
 | Standard integer range (`AbstractRange{T}`, `T` a standard integer) | `range`                                                 |
 | `Date`, `Time`, `DateTime` (from `Dates`)                           | `date`, `time`, `datetime` (from `datetime`)            |
 | `Second`, `Millisecond`, `Microsecond`, `Nanosecond` (from `Dates`) | `timedelta` (from `datetime`)                           |
-| `Number`                                                            | `juliacall.NumberValue`, `juliacall.ComplexValue`, etc. |
-| `AbstractArray`                                                     | `juliacall.JlArray`, `juliacall.JlVector`         |
-| `AbstractDict`                                                      | `juliacall.JlDict`                                   |
-| `AbstractSet`                                                       | `juliacall.JlSet`                                    |
-| `IO`                                                                | `juliacall.BufferedIOValue`                             |
-| `Module`                                                            | `juliacall.ModuleValue`                                 |
-| `Type`                                                              | `juliacall.TypeValue`                                   |
-| Anything else                                                       | `juliacall.Jl`                                    |
-
-See [here](@ref julia-wrappers) for an explanation of the `juliacall.*Value` wrapper types.
+| `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.Jl*` wrapper types.
 
 ## [Custom rules](@id jl2py-conversion-custom)
 
diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md
index 52889e49..d646b421 100644
--- a/docs/src/juliacall-reference.md
+++ b/docs/src/juliacall-reference.md
@@ -5,7 +5,7 @@
 `````@customdoc
 juliacall.Main - Constant
 
-The Julia `Main` module, as a [`ModuleValue`](#juliacall.ModuleValue).
+The Julia `Main` module, as a [`Jl`](#juliacall.Jl).
 
 In interactive scripts, you can use this as the main entry-point to JuliaCall:
 ```python
diff --git a/pytest/test_all.py b/pytest/test_all.py
index e531e321..2226df4c 100644
--- a/pytest/test_all.py
+++ b/pytest/test_all.py
@@ -5,7 +5,7 @@ def test_newmodule():
     import juliacall
     jl = juliacall.Main
     m = juliacall.newmodule("TestModule")
-    assert isinstance(m, juliacall.ModuleValue)
+    assert isinstance(m, juliacall.Jl)
     assert jl.isa(m, jl.Module)
     assert str(jl.nameof(m)) == "TestModule"
 

From 4edab030c655d9b8efa110cbef56190b95ff8bc6 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 22:04:20 +0100
Subject: [PATCH 36/39] enable CI on v1 branch

---
 .github/workflows/docs.yml  | 1 +
 .github/workflows/tests.yml | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8d57d8fa..69f30498 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
     - main
+    - v1
     tags:
     - '*'
   pull_request:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a3462b48..44f9e137 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -4,9 +4,11 @@ on:
   pull_request:
     branches:
     - main
+    - v1
   push:
     branches:
     - main
+    - v1
     tags:
     - '*'
 

From 8057efbff345e5edf39dd5eff4a4176a25984cf8 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 22:40:42 +0100
Subject: [PATCH 37/39] Jl.jl_eval fix return type

---
 src/JlWrap/any.jl |  2 +-
 test/JlWrap.jl    | 11 +++++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/JlWrap/any.jl b/src/JlWrap/any.jl
index 476aee65..6a521bae 100644
--- a/src/JlWrap/any.jl
+++ b/src/JlWrap/any.jl
@@ -235,7 +235,7 @@ function pyjlany_mimebundle(self, include::Py, exclude::Py)
     return ans
 end
 
-pyjlany_eval(self::Module, expr::Py) = Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
+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))
diff --git a/test/JlWrap.jl b/test/JlWrap.jl
index fb5ebc05..70e00e28 100644
--- a/test/JlWrap.jl
+++ b/test/JlWrap.jl
@@ -225,8 +225,14 @@
     end
     @testset "eval" begin
         m = pyjl(Main)
-        @test pyconvert(Any, m.jl_eval("1 + 1")) === 2 # Basic behavior
-        @test pyconvert(Any, m.jl_eval("1 + 1\n ")) === 2 # Trailing whitespace
+        # 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)
@@ -236,6 +242,7 @@
     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

From e5661bb9227435fb8978f08d64f3d2ab80c870bd Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 22:41:17 +0100
Subject: [PATCH 38/39] adapt python tests to latest version

---
 pysrc/juliacall/__init__.py |  2 +-
 pytest/test_all.py          | 26 +++++++++-----------------
 2 files changed, 10 insertions(+), 18 deletions(-)

diff --git a/pysrc/juliacall/__init__.py b/pysrc/juliacall/__init__.py
index 286ce5b9..9b7133d1 100644
--- a/pysrc/juliacall/__init__.py
+++ b/pysrc/juliacall/__init__.py
@@ -9,7 +9,7 @@ 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)
 
 def interactive(enable=True):
diff --git a/pytest/test_all.py b/pytest/test_all.py
index 2226df4c..00303587 100644
--- a/pytest/test_all.py
+++ b/pytest/test_all.py
@@ -9,14 +9,6 @@ def test_newmodule():
     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.Jl)
-        assert jl.isa(y, t)
-
 def test_interactive():
     import juliacall
     juliacall.interactive(True)
@@ -47,26 +39,26 @@ def test_issue_394():
     f = lambda x: x+1
     y = 5
     jl.x = x
-    assert jl.x is x
+    assert jl.x.jl_to_py() is x
     jl.f = f
-    assert jl.f is f
+    assert jl.f.jl_to_py() is f
     jl.y = y
-    assert jl.y is y
-    assert jl.x is x
-    assert jl.f is f
-    assert jl.y is y
-    assert jl.seval("f(x)") == 4
+    assert jl.y.jl_to_py() is y
+    assert jl.x.jl_to_py() is x
+    assert jl.f.jl_to_py() is f
+    assert jl.y.jl_to_py() is y
+    assert jl.jl_eval("f(x)").jl_to_py() == 4
 
 def test_issue_433():
     "https://github.com/JuliaPy/PythonCall.jl/issues/433"
     from juliacall import Main as jl
 
     # Smoke test
-    jl.seval("x=1\nx=1")
+    jl.jl_eval("x=1\nx=1")
     assert jl.x == 1
 
     # Do multiple things
-    out = jl.seval(
+    out = jl.jl_eval(
         """
         function _issue_433_g(x)
             return x^2

From dba0f37e8fa269f18c4a5c665337f613dda03632 Mon Sep 17 00:00:00 2001
From: Christopher Doris <github.com/cjdoris>
Date: Thu, 20 Jun 2024 22:57:10 +0100
Subject: [PATCH 39/39] propagate seval -> jl_eval

---
 docs/src/juliacall.md       |  4 ++--
 examples/flux.ipynb         |  4 ++--
 pysrc/juliacall/importer.py |  2 +-
 pysrc/juliacall/ipython.py  | 16 ++++++++--------
 4 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/docs/src/juliacall.md b/docs/src/juliacall.md
index d111a0c9..678a747d 100644
--- a/docs/src/juliacall.md
+++ b/docs/src/juliacall.md
@@ -50,11 +50,11 @@ import juliacall
 jl = juliacall.newmodule("SomeName")
 ```
 
-Julia modules have a special method `seval` which will evaluate a given piece of code given
+Julia modules have a special method `jl_eval` which will evaluate a given piece of code given
 as a string in the module. This is most frequently used to import modules:
 ```python
 from array import array
-jl.seval("using Statistics")
+jl.jl_eval("using Statistics")
 x = array('i', [1, 2, 3])
 jl.mean(x)
 # 2.0
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/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