Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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+.

Expand Down
5 changes: 5 additions & 0 deletions docs/src/juliacall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ from juliacall import Main as jl
# equivalent to Vector{Int}() in Julia
jl.Vector[jl.Int]()
```

Some Julia types can be converted to corresponding numpy dtypes like `numpy.dtype(jl.Int)`.
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
Expand Down
19 changes: 19 additions & 0 deletions src/JlWrap/type.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ function pyjltype_getitem(self::Type, k_)
end
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 pyisnull(descr)
return np.dtype(typestr)
else
return np.dtype(descr)
end
end

pyjl_handle_error_type(::typeof(pyjltype_numpy_dtype), x, exc) = pybuiltins.AttributeError

function init_type()
jl = pyjuliacallmodule
pybuiltins.exec(
Expand All @@ -25,6 +41,9 @@ class TypeValue(AnyValue):
raise TypeError("not supported")
def __delitem__(self, k):
raise TypeError("not supported")
@property
def __numpy_dtype__(self):
return self._jl_callmethod($(pyjl_methodnum(pyjltype_numpy_dtype)))
""",
@__FILE__(),
"exec",
Expand Down
61 changes: 60 additions & 1 deletion test/JlWrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -472,13 +472,72 @@ end
end
end

@testitem "type" begin
@testitem "type" setup = [Setup] begin
using PythonCall.NumpyDates
@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")

# 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"),
(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"),
(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))
end

# unsupported cases
@testset "$t -> AttributeError" for t in [
# non-primitives or mutables
String,
Vector{Int},
# pointers
Ptr{Cvoid},
Ptr{Int},
# PyPtr specifically should NOT be interpreted as np.dtype("O")
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
end
end
end

@testitem "vector" begin
Expand Down