diff --git a/src/API/exports.jl b/src/API/exports.jl index 3d476182..b5bc0f9e 100644 --- a/src/API/exports.jl +++ b/src/API/exports.jl @@ -120,6 +120,7 @@ export PyList export PyPandasDataFrame export PySet export PyTable +export PyTuple # JlWrap export pybinaryio diff --git a/src/API/types.jl b/src/API/types.jl index 55a4211e..f5e26839 100644 --- a/src/API/types.jl +++ b/src/API/types.jl @@ -181,6 +181,18 @@ struct PyList{T} <: AbstractVector{T} PyList{T}(x = pylist()) where {T} = new{T}(ispy(x) ? Py(x) : pylist(x)) end +""" + PyTuple{T<:Tuple=Tuple}([x]) + +Wraps the Python tuple or sequence `x`, with its elements described by the tuple type `T`. + +If `x` is not a Python object, it is converted to a Python tuple using [`pytuple`](@ref). +""" +struct PyTuple{T<:Tuple} + py::Py + PyTuple{T}(x = pytuple()) where {T<:Tuple} = new{T}(ispy(x) ? Py(x) : pytuple(x)) +end + """ PyTable(x) diff --git a/src/Wrap/PyTuple.jl b/src/Wrap/PyTuple.jl new file mode 100644 index 00000000..075da444 --- /dev/null +++ b/src/Wrap/PyTuple.jl @@ -0,0 +1,31 @@ +PyTuple(x = pytuple()) = PyTuple{Tuple}(x) + +ispy(::PyTuple) = true +Py(x::PyTuple) = x.py + +function pyconvert_rule_pytuple( + ::Type{T}, + x::Py, + ::Type{T1} = Utils._type_ub(T), +) where {T<:PyTuple,T1} + pyconvert_return(T1(x)) +end + +Base.IteratorSize(::Type{<:PyTuple}) = Base.HasLength() +Base.IteratorEltype(::Type{<:PyTuple}) = Base.HasEltype() +Base.eltype(::Type{PyTuple{T}}) where {T} = eltype(T) + +Base.length(x::PyTuple{T}) where {T} = Base.isvatuple(T) ? Int(pylen(x)) : fieldcount(T) +Base.size(x::PyTuple) = (length(x),) + +Base.@propagate_inbounds function Base.getindex(x::PyTuple{T}, i::Int) where {T} + @boundscheck (1 <= i <= length(x) || throw(BoundsError(x, i))) + return pyconvert(fieldtype(T, i), @py x[@jl(i - 1)]) +end + +function Base.iterate(x::PyTuple, i::Int = 1) + i > length(x) && return nothing + return (@inbounds(x[i]), i + 1) +end + +Base.Tuple(x::PyTuple{T}) where {T} = pyconvert(T, x.py) diff --git a/src/Wrap/Wrap.jl b/src/Wrap/Wrap.jl index d3b30ff2..a3d2e01b 100644 --- a/src/Wrap/Wrap.jl +++ b/src/Wrap/Wrap.jl @@ -14,7 +14,7 @@ using ..Convert using ..PyMacro import ..PythonCall: - PyArray, PyDict, PyIO, PyIterable, PyList, PyPandasDataFrame, PySet, PyTable + PyArray, PyDict, PyIO, PyIterable, PyList, PyPandasDataFrame, PySet, PyTable, PyTuple using Base: @propagate_inbounds using Tables: Tables @@ -25,6 +25,7 @@ import ..Core: Py, ispy include("PyIterable.jl") include("PyDict.jl") include("PyList.jl") +include("PyTuple.jl") include("PySet.jl") include("PyArray.jl") include("PyIO.jl") @@ -81,6 +82,12 @@ function __init__() pyconvert_add_rule("", AbstractArray, pyconvert_rule_array, priority) pyconvert_add_rule("", AbstractArray, pyconvert_rule_array, priority) pyconvert_add_rule("", AbstractArray, pyconvert_rule_array, priority) + pyconvert_add_rule( + "collections.abc:Sequence", + PyTuple, + pyconvert_rule_pytuple, + priority, + ) end end diff --git a/test/Convert.jl b/test/Convert.jl index 2765440f..6fca0ff2 100644 --- a/test/Convert.jl +++ b/test/Convert.jl @@ -196,6 +196,15 @@ end @test x2 == [1, 2, 3] end +@testitem "sequence → PyTuple" begin + x1 = pyconvert(PyTuple, pylist([1, 2, 3])) + @test x1 isa PyTuple{Tuple} + @test Tuple(x1) == (1, 2, 3) + x2 = pyconvert(PyTuple{Tuple{Int,Float64}}, pytuple([1, 2])) + @test x2 isa PyTuple{Tuple{Int,Float64}} + @test Tuple(x2) === (1, 2.0) +end + @testitem "set → PySet" begin x1 = pyconvert(PySet, pyset([1, 2, 3])) @test x1 isa PySet{Any} diff --git a/test/Wrap.jl b/test/Wrap.jl index 09d6ece7..2fef2f29 100644 --- a/test/Wrap.jl +++ b/test/Wrap.jl @@ -468,6 +468,35 @@ end end end +@testitem "PyTuple" begin + x = pytuple([1, "two", 3]) + y = PyTuple(x) + z = PyTuple{Tuple{Int,String,Float64}}(x) + @test y isa PyTuple{Tuple} + @test z isa PyTuple{Tuple{Int,String,Float64}} + @test PythonCall.ispy(z) + @test Py(z) === x + @test length(z) == 3 + @test size(z) == (3,) + @test eltype(z) === Any + @test z[1] === 1 + @test z[2] == "two" + @test z[3] === 3.0 + @test_throws BoundsError z[0] + @test_throws BoundsError z[4] + @test collect(z) == Any[1, "two", 3.0] + @test Tuple(z) === (1, "two", 3.0) + + v = PyTuple{Tuple{Int,Vararg{String}}}(pylist([1, "two", "three"])) + @test length(v) == 3 + @test v[3] == "three" + @test Tuple(v) === (1, "two", "three") + + fixed = PyTuple{Tuple{Vararg{Int,2}}}([4, 5]) + @test length(fixed) == 2 + @test Tuple(fixed) === (4, 5) +end + @testitem "PyPandasDataFrame" setup=[Setup] begin using Tables @test PyPandasDataFrame isa Type