From fa80da6d1c900cbcae0a310b8808837c84505c21 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Thu, 22 Jan 2026 22:08:07 +0000 Subject: [PATCH 1/8] Add numpy dtype mapping for TypeValue --- pytest/test_all.py | 27 +++++++++++++++++++++++++++ src/JlWrap/type.jl | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/pytest/test_all.py b/pytest/test_all.py index 9cdc8ce4..006da4ed 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -25,6 +25,33 @@ def test_convert(): assert jl.isa(y, t) +def test_typevalue_numpy_dtype(): + import numpy as np + from juliacall import Base as jl + + assert jl.Bool.__numpy_dtype__ == np.dtype(np.bool_) + assert jl.Int8.__numpy_dtype__ == np.dtype(np.int8) + assert jl.Int16.__numpy_dtype__ == np.dtype(np.int16) + assert jl.Int32.__numpy_dtype__ == np.dtype(np.int32) + assert jl.Int64.__numpy_dtype__ == np.dtype(np.int64) + assert jl.Int.__numpy_dtype__ == np.dtype(np.int_) + assert jl.UInt8.__numpy_dtype__ == np.dtype(np.uint8) + assert jl.UInt16.__numpy_dtype__ == np.dtype(np.uint16) + assert jl.UInt32.__numpy_dtype__ == np.dtype(np.uint32) + assert jl.UInt64.__numpy_dtype__ == np.dtype(np.uint64) + assert jl.UInt.__numpy_dtype__ == np.dtype(np.uintp) + assert jl.Float16.__numpy_dtype__ == np.dtype(np.float16) + assert jl.Float32.__numpy_dtype__ == np.dtype(np.float32) + assert jl.Float64.__numpy_dtype__ == np.dtype(np.float64) + assert jl.ComplexF32.__numpy_dtype__ == np.dtype(np.complex64) + assert jl.ComplexF64.__numpy_dtype__ == np.dtype(np.complex128) + assert jl.Ptr[jl.Cvoid].__numpy_dtype__ == np.dtype("P") + with pytest.raises(AttributeError): + _ = jl.ComplexF16.__numpy_dtype__ + with pytest.raises(AttributeError): + _ = jl.String.__numpy_dtype__ + + def test_interactive(): import juliacall diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl index 72a2f992..dee75542 100644 --- a/src/JlWrap/type.jl +++ b/src/JlWrap/type.jl @@ -25,6 +25,44 @@ class TypeValue(AnyValue): raise TypeError("not supported") def __delitem__(self, k): raise TypeError("not supported") + @property + def __numpy_dtype__(self): + import numpy + if self == Base.Bool: + return numpy.dtype(numpy.bool_) + if self == Base.Int8: + return numpy.dtype(numpy.int8) + if self == Base.Int16: + return numpy.dtype(numpy.int16) + if self == Base.Int32: + return numpy.dtype(numpy.int32) + if self == Base.Int64: + return numpy.dtype(numpy.int64) + if self == Base.Int: + return numpy.dtype(numpy.int_) + if self == Base.UInt8: + return numpy.dtype(numpy.uint8) + if self == Base.UInt16: + return numpy.dtype(numpy.uint16) + if self == Base.UInt32: + return numpy.dtype(numpy.uint32) + if self == Base.UInt64: + return numpy.dtype(numpy.uint64) + if self == Base.UInt: + return numpy.dtype(numpy.uintp) + if self == Base.Float16: + return numpy.dtype(numpy.float16) + if self == Base.Float32: + return numpy.dtype(numpy.float32) + if self == Base.Float64: + return numpy.dtype(numpy.float64) + if self == Base.ComplexF32: + return numpy.dtype(numpy.complex64) + if self == Base.ComplexF64: + return numpy.dtype(numpy.complex128) + if self == Base.Ptr[Base.Cvoid]: + return numpy.dtype("P") + raise AttributeError("__numpy_dtype__") """, @__FILE__(), "exec", From 946f6a8e4edbbdd3a6f2577c28ff0b2fc0ba1647 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Thu, 22 Jan 2026 22:29:09 +0000 Subject: [PATCH 2/8] Move numpy dtype mapping to Julia --- docs/src/juliacall-reference.md | 4 ++ pytest/test_all.py | 27 ----------- src/JlWrap/type.jl | 86 +++++++++++++++++++-------------- test/JlWrap.jl | 45 ++++++++++++++++- 4 files changed, 98 insertions(+), 64 deletions(-) diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md index 0aa8e6d7..4b6a98a6 100644 --- a/docs/src/juliacall-reference.md +++ b/docs/src/juliacall-reference.md @@ -200,6 +200,10 @@ from juliacall import Main as jl # equivalent to Vector{Int}() in Julia jl.Vector[jl.Int]() ``` + +If NumPy is available, primitive types expose a `__numpy_dtype__` property that returns the +corresponding `numpy.dtype` (e.g. `jl.Int64.__numpy_dtype__`). Unsupported types raise +`AttributeError`. ````` `````@customdoc diff --git a/pytest/test_all.py b/pytest/test_all.py index 006da4ed..9cdc8ce4 100644 --- a/pytest/test_all.py +++ b/pytest/test_all.py @@ -25,33 +25,6 @@ def test_convert(): assert jl.isa(y, t) -def test_typevalue_numpy_dtype(): - import numpy as np - from juliacall import Base as jl - - assert jl.Bool.__numpy_dtype__ == np.dtype(np.bool_) - assert jl.Int8.__numpy_dtype__ == np.dtype(np.int8) - assert jl.Int16.__numpy_dtype__ == np.dtype(np.int16) - assert jl.Int32.__numpy_dtype__ == np.dtype(np.int32) - assert jl.Int64.__numpy_dtype__ == np.dtype(np.int64) - assert jl.Int.__numpy_dtype__ == np.dtype(np.int_) - assert jl.UInt8.__numpy_dtype__ == np.dtype(np.uint8) - assert jl.UInt16.__numpy_dtype__ == np.dtype(np.uint16) - assert jl.UInt32.__numpy_dtype__ == np.dtype(np.uint32) - assert jl.UInt64.__numpy_dtype__ == np.dtype(np.uint64) - assert jl.UInt.__numpy_dtype__ == np.dtype(np.uintp) - assert jl.Float16.__numpy_dtype__ == np.dtype(np.float16) - assert jl.Float32.__numpy_dtype__ == np.dtype(np.float32) - assert jl.Float64.__numpy_dtype__ == np.dtype(np.float64) - assert jl.ComplexF32.__numpy_dtype__ == np.dtype(np.complex64) - assert jl.ComplexF64.__numpy_dtype__ == np.dtype(np.complex128) - assert jl.Ptr[jl.Cvoid].__numpy_dtype__ == np.dtype("P") - with pytest.raises(AttributeError): - _ = jl.ComplexF16.__numpy_dtype__ - with pytest.raises(AttributeError): - _ = jl.String.__numpy_dtype__ - - def test_interactive(): import juliacall diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl index dee75542..8a70c11d 100644 --- a/src/JlWrap/type.jl +++ b/src/JlWrap/type.jl @@ -11,6 +11,55 @@ function pyjltype_getitem(self::Type, k_) end end +function pyjltype_numpy_dtype(self::Type) + np = pyimport("numpy") + if self === Bool + return np.dtype(np.bool_) + elseif self === Int8 + return np.dtype(np.int8) + elseif self === Int16 + return np.dtype(np.int16) + elseif self === Int32 + return np.dtype(np.int32) + elseif self === Int64 + return np.dtype(np.int64) + elseif self === UInt8 + return np.dtype(np.uint8) + elseif self === UInt16 + return np.dtype(np.uint16) + elseif self === UInt32 + return np.dtype(np.uint32) + elseif self === UInt64 + return np.dtype(np.uint64) + elseif self === Float16 + return np.dtype(np.float16) + elseif self === Float32 + return np.dtype(np.float32) + elseif self === Float64 + return np.dtype(np.float64) + elseif self === ComplexF32 + return np.dtype(np.complex64) + elseif self === ComplexF64 + return np.dtype(np.complex128) + elseif self === Ptr{Cvoid} + return np.dtype("P") + end + @static if Int !== Int64 + if self === Int + return np.dtype(np.int_) + end + end + @static if UInt !== UInt64 + if self === UInt + return np.dtype(np.uintp) + end + end + errset(pybuiltins.AttributeError, "__numpy_dtype__") + return PyNULL +end + +pyjl_handle_error_type(::typeof(pyjltype_numpy_dtype), x, exc) = pybuiltins.AttributeError + function init_type() jl = pyjuliacallmodule pybuiltins.exec( @@ -27,42 +76,7 @@ class TypeValue(AnyValue): raise TypeError("not supported") @property def __numpy_dtype__(self): - import numpy - if self == Base.Bool: - return numpy.dtype(numpy.bool_) - if self == Base.Int8: - return numpy.dtype(numpy.int8) - if self == Base.Int16: - return numpy.dtype(numpy.int16) - if self == Base.Int32: - return numpy.dtype(numpy.int32) - if self == Base.Int64: - return numpy.dtype(numpy.int64) - if self == Base.Int: - return numpy.dtype(numpy.int_) - if self == Base.UInt8: - return numpy.dtype(numpy.uint8) - if self == Base.UInt16: - return numpy.dtype(numpy.uint16) - if self == Base.UInt32: - return numpy.dtype(numpy.uint32) - if self == Base.UInt64: - return numpy.dtype(numpy.uint64) - if self == Base.UInt: - return numpy.dtype(numpy.uintp) - if self == Base.Float16: - return numpy.dtype(numpy.float16) - if self == Base.Float32: - return numpy.dtype(numpy.float32) - if self == Base.Float64: - return numpy.dtype(numpy.float64) - if self == Base.ComplexF32: - return numpy.dtype(numpy.complex64) - if self == Base.ComplexF64: - return numpy.dtype(numpy.complex128) - if self == Base.Ptr[Base.Cvoid]: - return numpy.dtype("P") - raise AttributeError("__numpy_dtype__") + return self._jl_callmethod($(pyjl_methodnum(pyjltype_numpy_dtype))) """, @__FILE__(), "exec", diff --git a/test/JlWrap.jl b/test/JlWrap.jl index 52d93642..81b45d71 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -472,13 +472,56 @@ end end end -@testitem "type" begin +@testitem "type" setup=[Setup] begin @testset "type" begin @test pyis(pytype(pyjl(Int)), PythonCall.pyjltypetype) end @testset "bool" begin @test pytruth(pyjl(Int)) end + @testset "numpy dtype" begin + if Setup.devdeps + np = pyimport("numpy") + @test pyeq(Bool, pygetattr(pyjl(Bool), "__numpy_dtype__"), np.dtype(np.bool_)) + @test pyeq(Bool, pygetattr(pyjl(Int8), "__numpy_dtype__"), np.dtype(np.int8)) + @test pyeq(Bool, pygetattr(pyjl(Int16), "__numpy_dtype__"), np.dtype(np.int16)) + @test pyeq(Bool, pygetattr(pyjl(Int32), "__numpy_dtype__"), np.dtype(np.int32)) + @test pyeq(Bool, pygetattr(pyjl(Int64), "__numpy_dtype__"), np.dtype(np.int64)) + @test pyeq(Bool, pygetattr(pyjl(Int), "__numpy_dtype__"), np.dtype(np.int_)) + @test pyeq(Bool, pygetattr(pyjl(UInt8), "__numpy_dtype__"), np.dtype(np.uint8)) + @test pyeq(Bool, pygetattr(pyjl(UInt16), "__numpy_dtype__"), np.dtype(np.uint16)) + @test pyeq(Bool, pygetattr(pyjl(UInt32), "__numpy_dtype__"), np.dtype(np.uint32)) + @test pyeq(Bool, pygetattr(pyjl(UInt64), "__numpy_dtype__"), np.dtype(np.uint64)) + @test pyeq(Bool, pygetattr(pyjl(UInt), "__numpy_dtype__"), np.dtype(np.uintp)) + @test pyeq(Bool, pygetattr(pyjl(Float16), "__numpy_dtype__"), np.dtype(np.float16)) + @test pyeq(Bool, pygetattr(pyjl(Float32), "__numpy_dtype__"), np.dtype(np.float32)) + @test pyeq(Bool, pygetattr(pyjl(Float64), "__numpy_dtype__"), np.dtype(np.float64)) + @test pyeq(Bool, pygetattr(pyjl(ComplexF32), "__numpy_dtype__"), np.dtype(np.complex64)) + @test pyeq(Bool, pygetattr(pyjl(ComplexF64), "__numpy_dtype__"), np.dtype(np.complex128)) + @test pyeq(Bool, pygetattr(pyjl(Ptr{Cvoid}), "__numpy_dtype__"), np.dtype("P")) + @test pyeq(Bool, np.dtype(pyjl(Int64)), np.dtype(np.int64)) + + err = try + pygetattr(pyjl(ComplexF16), "__numpy_dtype__") + nothing + catch err + err + end + @test err isa PythonCall.PyException + @test pyis(err._t, pybuiltins.AttributeError) + + err = try + pygetattr(pyjl(String), "__numpy_dtype__") + nothing + catch err + err + end + @test err isa PythonCall.PyException + @test pyis(err._t, pybuiltins.AttributeError) + else + @test_skip Setup.devdeps + end + end end @testitem "vector" begin From d969fbce205ea3d0b3534911bd50ed9874e9225f Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 10:56:36 +0000 Subject: [PATCH 3/8] tidying --- docs/src/juliacall-reference.md | 6 +-- src/JlWrap/type.jl | 10 ----- test/JlWrap.jl | 72 +++++++++++++++++---------------- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md index 4b6a98a6..fc474849 100644 --- a/docs/src/juliacall-reference.md +++ b/docs/src/juliacall-reference.md @@ -201,9 +201,9 @@ from juliacall import Main as jl jl.Vector[jl.Int]() ``` -If NumPy is available, primitive types expose a `__numpy_dtype__` property that returns the -corresponding `numpy.dtype` (e.g. `jl.Int64.__numpy_dtype__`). Unsupported types raise -`AttributeError`. +Some Julia types can be converted to corresponding numpy dtypes like `numpy.dtype(jl.Int)`. +Currently supports these primitive types: `Bool`, `IntXX`, `UIntXX`, `FloatXX`, +`ComplexFXX`, `Ptr{Cvoid}`. ````` `````@customdoc diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl index 8a70c11d..11bde298 100644 --- a/src/JlWrap/type.jl +++ b/src/JlWrap/type.jl @@ -44,16 +44,6 @@ function pyjltype_numpy_dtype(self::Type) elseif self === Ptr{Cvoid} return np.dtype("P") end - @static if Int !== Int64 - if self === Int - return np.dtype(np.int_) - end - end - @static if UInt !== UInt64 - if self === UInt - return np.dtype(np.uintp) - end - end errset(pybuiltins.AttributeError, "__numpy_dtype__") return PyNULL end diff --git a/test/JlWrap.jl b/test/JlWrap.jl index 81b45d71..2be308c7 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -472,7 +472,7 @@ end end end -@testitem "type" setup=[Setup] begin +@testitem "type" setup = [Setup] begin @testset "type" begin @test pyis(pytype(pyjl(Int)), PythonCall.pyjltypetype) end @@ -482,44 +482,46 @@ end @testset "numpy dtype" begin if Setup.devdeps np = pyimport("numpy") - @test pyeq(Bool, pygetattr(pyjl(Bool), "__numpy_dtype__"), np.dtype(np.bool_)) - @test pyeq(Bool, pygetattr(pyjl(Int8), "__numpy_dtype__"), np.dtype(np.int8)) - @test pyeq(Bool, pygetattr(pyjl(Int16), "__numpy_dtype__"), np.dtype(np.int16)) - @test pyeq(Bool, pygetattr(pyjl(Int32), "__numpy_dtype__"), np.dtype(np.int32)) - @test pyeq(Bool, pygetattr(pyjl(Int64), "__numpy_dtype__"), np.dtype(np.int64)) - @test pyeq(Bool, pygetattr(pyjl(Int), "__numpy_dtype__"), np.dtype(np.int_)) - @test pyeq(Bool, pygetattr(pyjl(UInt8), "__numpy_dtype__"), np.dtype(np.uint8)) - @test pyeq(Bool, pygetattr(pyjl(UInt16), "__numpy_dtype__"), np.dtype(np.uint16)) - @test pyeq(Bool, pygetattr(pyjl(UInt32), "__numpy_dtype__"), np.dtype(np.uint32)) - @test pyeq(Bool, pygetattr(pyjl(UInt64), "__numpy_dtype__"), np.dtype(np.uint64)) - @test pyeq(Bool, pygetattr(pyjl(UInt), "__numpy_dtype__"), np.dtype(np.uintp)) - @test pyeq(Bool, pygetattr(pyjl(Float16), "__numpy_dtype__"), np.dtype(np.float16)) - @test pyeq(Bool, pygetattr(pyjl(Float32), "__numpy_dtype__"), np.dtype(np.float32)) - @test pyeq(Bool, pygetattr(pyjl(Float64), "__numpy_dtype__"), np.dtype(np.float64)) - @test pyeq(Bool, pygetattr(pyjl(ComplexF32), "__numpy_dtype__"), np.dtype(np.complex64)) - @test pyeq(Bool, pygetattr(pyjl(ComplexF64), "__numpy_dtype__"), np.dtype(np.complex128)) - @test pyeq(Bool, pygetattr(pyjl(Ptr{Cvoid}), "__numpy_dtype__"), np.dtype("P")) - @test pyeq(Bool, np.dtype(pyjl(Int64)), np.dtype(np.int64)) - err = try - pygetattr(pyjl(ComplexF16), "__numpy_dtype__") - nothing - catch err - err + # success cases + @testset "$t -> $d" for (t, d) in [ + (Bool, "bool"), + (Int8, "int8"), + (Int16, "int16"), + (Int32, "int32"), + (Int64, "int64"), + (UInt8, "uint8"), + (UInt16, "uint16"), + (UInt32, "uint32"), + (UInt64, "uint64"), + (Float16, "float16"), + (Float32, "float32"), + (Float64, "float64"), + (ComplexF32, "complex64"), + (ComplexF64, "complex128"), + (Ptr{Cvoid}, "P"), + ] + @test pyeq(Bool, pygetattr(pyjl(t), "__numpy_dtype__"), np.dtype(d)) + @test pyeq(Bool, np.dtype(pyjl(t)), np.dtype(d)) end - @test err isa PythonCall.PyException - @test pyis(err._t, pybuiltins.AttributeError) - err = try - pygetattr(pyjl(String), "__numpy_dtype__") - nothing - catch err - err + # unsupported cases + @testset "$t -> AttributeError" for t in [ + ComplexF16, + String, + Tuple{}, + Ptr{Int}, + Ptr{PythonCall.C.PyPtr}, + ] + err = try + pygetattr(pyjl(t), "__numpy_dtype__") + nothing + catch err + err + end + @test err isa PythonCall.PyException + @test pyis(err.t, pybuiltins.AttributeError) end - @test err isa PythonCall.PyException - @test pyis(err._t, pybuiltins.AttributeError) - else - @test_skip Setup.devdeps end end end From 63312d729d62c6ae2a5d22f177ab298b84d956b1 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 11:50:32 +0000 Subject: [PATCH 4/8] add support for datetime64 and timedelta64 --- docs/src/juliacall-reference.md | 2 +- src/JlWrap/type.jl | 41 ++++++++------------------------- test/JlWrap.jl | 12 +++++++--- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md index fc474849..d82d18e2 100644 --- a/docs/src/juliacall-reference.md +++ b/docs/src/juliacall-reference.md @@ -203,7 +203,7 @@ jl.Vector[jl.Int]() Some Julia types can be converted to corresponding numpy dtypes like `numpy.dtype(jl.Int)`. Currently supports these primitive types: `Bool`, `IntXX`, `UIntXX`, `FloatXX`, -`ComplexFXX`, `Ptr{Cvoid}`. +`ComplexFXX`, `NumpyDates.InlineDateTime64{unit}` and `NumpyDates.InlineTimeDelta64{unit}`. ````` `````@customdoc diff --git a/src/JlWrap/type.jl b/src/JlWrap/type.jl index 11bde298..00bbf058 100644 --- a/src/JlWrap/type.jl +++ b/src/JlWrap/type.jl @@ -12,40 +12,17 @@ function pyjltype_getitem(self::Type, k_) end function pyjltype_numpy_dtype(self::Type) + typestr, descr = pytypestrdescr(self) + if isempty(typestr) + errset(pybuiltins.AttributeError, "__numpy_dtype__") + return PyNULL + end np = pyimport("numpy") - if self === Bool - return np.dtype(np.bool_) - elseif self === Int8 - return np.dtype(np.int8) - elseif self === Int16 - return np.dtype(np.int16) - elseif self === Int32 - return np.dtype(np.int32) - elseif self === Int64 - return np.dtype(np.int64) - elseif self === UInt8 - return np.dtype(np.uint8) - elseif self === UInt16 - return np.dtype(np.uint16) - elseif self === UInt32 - return np.dtype(np.uint32) - elseif self === UInt64 - return np.dtype(np.uint64) - elseif self === Float16 - return np.dtype(np.float16) - elseif self === Float32 - return np.dtype(np.float32) - elseif self === Float64 - return np.dtype(np.float64) - elseif self === ComplexF32 - return np.dtype(np.complex64) - elseif self === ComplexF64 - return np.dtype(np.complex128) - elseif self === Ptr{Cvoid} - return np.dtype("P") + if pyisnull(descr) + return np.dtype(typestr) + else + return np.dtype(descr) end - errset(pybuiltins.AttributeError, "__numpy_dtype__") - return PyNULL end pyjl_handle_error_type(::typeof(pyjltype_numpy_dtype), x, exc) = pybuiltins.AttributeError diff --git a/test/JlWrap.jl b/test/JlWrap.jl index 2be308c7..7296a084 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -473,6 +473,7 @@ end end @testitem "type" setup = [Setup] begin + using PythonCall.NumpyDates @testset "type" begin @test pyis(pytype(pyjl(Int)), PythonCall.pyjltypetype) end @@ -499,7 +500,12 @@ end (Float64, "float64"), (ComplexF32, "complex64"), (ComplexF64, "complex128"), - (Ptr{Cvoid}, "P"), + (InlineDateTime64{SECONDS}, "datetime64[s]"), + (InlineDateTime64{(SECONDS, 5)}, "datetime64[5s]"), + (InlineDateTime64{NumpyDates.UNBOUND_UNITS}, "datetime64"), + (InlineTimeDelta64{MINUTES}, "timedelta64[m]"), + (InlineTimeDelta64{(SECONDS, 5)}, "timedelta64[5s]"), + (InlineTimeDelta64{NumpyDates.UNBOUND_UNITS}, "timedelta64"), ] @test pyeq(Bool, pygetattr(pyjl(t), "__numpy_dtype__"), np.dtype(d)) @test pyeq(Bool, np.dtype(pyjl(t)), np.dtype(d)) @@ -507,9 +513,9 @@ end # unsupported cases @testset "$t -> AttributeError" for t in [ - ComplexF16, String, - Tuple{}, + Vector{Int}, + Ptr{Cvoid}, Ptr{Int}, Ptr{PythonCall.C.PyPtr}, ] From c2ea0442e502b11dcd82dc211cdd014d43d98f79 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 11:54:38 +0000 Subject: [PATCH 5/8] add tests for struct types --- test/JlWrap.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/JlWrap.jl b/test/JlWrap.jl index 7296a084..dff476f9 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -506,6 +506,11 @@ end (InlineTimeDelta64{MINUTES}, "timedelta64[m]"), (InlineTimeDelta64{(SECONDS, 5)}, "timedelta64[5s]"), (InlineTimeDelta64{NumpyDates.UNBOUND_UNITS}, "timedelta64"), + (Tuple{}, np.dtype(pylist())), + (Tuple{Int32, Int32}, np.dtype(pylist([("f0", "int32"), ("f1", "int32")]))), + (@NamedTuple{}, np.dtype(pylist())), + (@NamedTuple{x::Int32, y::Int32}, np.dtype(pylist([("x", "int32"), ("y", "int32")]))), + (Pair{Int32, Int32}, np.dtype(pylist([("first", "int32"), ("second", "int32")]))), ] @test pyeq(Bool, pygetattr(pyjl(t), "__numpy_dtype__"), np.dtype(d)) @test pyeq(Bool, np.dtype(pyjl(t)), np.dtype(d)) From 522869f8cbacd4a8788515114dd31577ff00aef5 Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 11:56:34 +0000 Subject: [PATCH 6/8] document struct support --- docs/src/juliacall-reference.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/juliacall-reference.md b/docs/src/juliacall-reference.md index d82d18e2..ef5e5e5c 100644 --- a/docs/src/juliacall-reference.md +++ b/docs/src/juliacall-reference.md @@ -202,8 +202,9 @@ jl.Vector[jl.Int]() ``` Some Julia types can be converted to corresponding numpy dtypes like `numpy.dtype(jl.Int)`. -Currently supports these primitive types: `Bool`, `IntXX`, `UIntXX`, `FloatXX`, -`ComplexFXX`, `NumpyDates.InlineDateTime64{unit}` and `NumpyDates.InlineTimeDelta64{unit}`. +Supports primitive types: `Bool`, `IntXX`, `UIntXX`, `FloatXX`, `ComplexFXX`, +`NumpyDates.InlineDateTime64{unit}` and `NumpyDates.InlineTimeDelta64{unit}`. Also +supports tuples, named tuples and structs of these. ````` `````@customdoc From 092564199a288da1374802aaf44ea5758358b80d Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 12:04:22 +0000 Subject: [PATCH 7/8] test simplifications and comments --- test/JlWrap.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/JlWrap.jl b/test/JlWrap.jl index dff476f9..818f83de 100644 --- a/test/JlWrap.jl +++ b/test/JlWrap.jl @@ -506,11 +506,11 @@ end (InlineTimeDelta64{MINUTES}, "timedelta64[m]"), (InlineTimeDelta64{(SECONDS, 5)}, "timedelta64[5s]"), (InlineTimeDelta64{NumpyDates.UNBOUND_UNITS}, "timedelta64"), - (Tuple{}, np.dtype(pylist())), - (Tuple{Int32, Int32}, np.dtype(pylist([("f0", "int32"), ("f1", "int32")]))), - (@NamedTuple{}, np.dtype(pylist())), - (@NamedTuple{x::Int32, y::Int32}, np.dtype(pylist([("x", "int32"), ("y", "int32")]))), - (Pair{Int32, Int32}, np.dtype(pylist([("first", "int32"), ("second", "int32")]))), + (Tuple{}, pylist()), + (Tuple{Int32, Int32}, pylist([("f0", "int32"), ("f1", "int32")])), + (@NamedTuple{}, pylist()), + (@NamedTuple{x::Int32, y::Int32}, pylist([("x", "int32"), ("y", "int32")])), + (Pair{Int32, Int32}, pylist([("first", "int32"), ("second", "int32")])), ] @test pyeq(Bool, pygetattr(pyjl(t), "__numpy_dtype__"), np.dtype(d)) @test pyeq(Bool, np.dtype(pyjl(t)), np.dtype(d)) @@ -518,11 +518,14 @@ end # unsupported cases @testset "$t -> AttributeError" for t in [ + # non-primitives or mutables String, Vector{Int}, + # pointers Ptr{Cvoid}, Ptr{Int}, - Ptr{PythonCall.C.PyPtr}, + # PyPtr specifically should NOT be interpreted as np.dtype("O") + PythonCall.C.PyPtr, ] err = try pygetattr(pyjl(t), "__numpy_dtype__") From 5ee46acc444e58bc045f7acb15248f9ecc0b2a4c Mon Sep 17 00:00:00 2001 From: Christopher Rowley Date: Fri, 23 Jan 2026 12:10:45 +0000 Subject: [PATCH 8/8] update changelog [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d3bee8..56609490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased +* Added `juliacall.TypeValue.__numpy_dtype__` attribute to allow converting Julia types + to the corresponding NumPy dtype, like `numpy.dtype(jl.Int)`. + ## 0.9.31 (2025-12-17) * Restore support for Python 3.14+.