Skip to content

Commit 82920fe

Browse files
committed
Add typed PyTuple sequence wrapper
1 parent de68e38 commit 82920fe

6 files changed

Lines changed: 90 additions & 1 deletion

File tree

‎src/API/exports.jl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export PyList
120120
export PyPandasDataFrame
121121
export PySet
122122
export PyTable
123+
export PyTuple
123124

124125
# JlWrap
125126
export pybinaryio

‎src/API/types.jl‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ struct PyList{T} <: AbstractVector{T}
181181
PyList{T}(x = pylist()) where {T} = new{T}(ispy(x) ? Py(x) : pylist(x))
182182
end
183183

184+
"""
185+
PyTuple{T<:Tuple=Tuple}([x])
186+
187+
Wraps the Python tuple or sequence `x`, with its elements described by the tuple type `T`.
188+
189+
If `x` is not a Python object, it is converted to a Python tuple using [`pytuple`](@ref).
190+
"""
191+
struct PyTuple{T<:Tuple}
192+
py::Py
193+
PyTuple{T}(x = pytuple()) where {T<:Tuple} = new{T}(ispy(x) ? Py(x) : pytuple(x))
194+
end
195+
184196
"""
185197
PyTable(x)
186198

‎src/Wrap/PyTuple.jl‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
PyTuple(x = pytuple()) = PyTuple{Tuple}(x)
2+
3+
ispy(::PyTuple) = true
4+
Py(x::PyTuple) = x.py
5+
6+
function pyconvert_rule_pytuple(
7+
::Type{T},
8+
x::Py,
9+
::Type{T1} = Utils._type_ub(T),
10+
) where {T<:PyTuple,T1}
11+
pyconvert_return(T1(x))
12+
end
13+
14+
Base.IteratorSize(::Type{<:PyTuple}) = Base.HasLength()
15+
Base.IteratorEltype(::Type{<:PyTuple}) = Base.HasEltype()
16+
Base.eltype(::Type{PyTuple{T}}) where {T} = eltype(T)
17+
18+
Base.length(x::PyTuple{T}) where {T} = Base.isvatuple(T) ? Int(pylen(x)) : fieldcount(T)
19+
Base.size(x::PyTuple) = (length(x),)
20+
21+
Base.@propagate_inbounds function Base.getindex(x::PyTuple{T}, i::Int) where {T}
22+
@boundscheck (1 <= i <= length(x) || throw(BoundsError(x, i)))
23+
return pyconvert(fieldtype(T, i), @py x[@jl(i - 1)])
24+
end
25+
26+
function Base.iterate(x::PyTuple, i::Int = 1)
27+
i > length(x) && return nothing
28+
return (@inbounds(x[i]), i + 1)
29+
end
30+
31+
Base.Tuple(x::PyTuple{T}) where {T} = pyconvert(T, x.py)

‎src/Wrap/Wrap.jl‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ using ..Convert
1414
using ..PyMacro
1515

1616
import ..PythonCall:
17-
PyArray, PyDict, PyIO, PyIterable, PyList, PyPandasDataFrame, PySet, PyTable
17+
PyArray, PyDict, PyIO, PyIterable, PyList, PyPandasDataFrame, PySet, PyTable, PyTuple
1818

1919
using Base: @propagate_inbounds
2020
using Tables: Tables
@@ -25,6 +25,7 @@ import ..Core: Py, ispy
2525
include("PyIterable.jl")
2626
include("PyDict.jl")
2727
include("PyList.jl")
28+
include("PyTuple.jl")
2829
include("PySet.jl")
2930
include("PyArray.jl")
3031
include("PyIO.jl")
@@ -81,6 +82,12 @@ function __init__()
8182
pyconvert_add_rule("<arrayinterface>", AbstractArray, pyconvert_rule_array, priority)
8283
pyconvert_add_rule("<array>", AbstractArray, pyconvert_rule_array, priority)
8384
pyconvert_add_rule("<buffer>", AbstractArray, pyconvert_rule_array, priority)
85+
pyconvert_add_rule(
86+
"collections.abc:Sequence",
87+
PyTuple,
88+
pyconvert_rule_pytuple,
89+
priority,
90+
)
8491
end
8592

8693
end

‎test/Convert.jl‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,15 @@ end
196196
@test x2 == [1, 2, 3]
197197
end
198198

199+
@testitem "sequence → PyTuple" begin
200+
x1 = pyconvert(PyTuple, pylist([1, 2, 3]))
201+
@test x1 isa PyTuple{Tuple}
202+
@test Tuple(x1) == (1, 2, 3)
203+
x2 = pyconvert(PyTuple{Tuple{Int,Float64}}, pytuple([1, 2]))
204+
@test x2 isa PyTuple{Tuple{Int,Float64}}
205+
@test Tuple(x2) === (1, 2.0)
206+
end
207+
199208
@testitem "set → PySet" begin
200209
x1 = pyconvert(PySet, pyset([1, 2, 3]))
201210
@test x1 isa PySet{Any}

‎test/Wrap.jl‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,35 @@ end
468468
end
469469
end
470470

471+
@testitem "PyTuple" begin
472+
x = pytuple([1, "two", 3])
473+
y = PyTuple(x)
474+
z = PyTuple{Tuple{Int,String,Float64}}(x)
475+
@test y isa PyTuple{Tuple}
476+
@test z isa PyTuple{Tuple{Int,String,Float64}}
477+
@test PythonCall.ispy(z)
478+
@test Py(z) === x
479+
@test length(z) == 3
480+
@test size(z) == (3,)
481+
@test eltype(z) === Any
482+
@test z[1] === 1
483+
@test z[2] == "two"
484+
@test z[3] === 3.0
485+
@test_throws BoundsError z[0]
486+
@test_throws BoundsError z[4]
487+
@test collect(z) == Any[1, "two", 3.0]
488+
@test Tuple(z) === (1, "two", 3.0)
489+
490+
v = PyTuple{Tuple{Int,Vararg{String}}}(pylist([1, "two", "three"]))
491+
@test length(v) == 3
492+
@test v[3] == "three"
493+
@test Tuple(v) === (1, "two", "three")
494+
495+
fixed = PyTuple{Tuple{Vararg{Int,2}}}([4, 5])
496+
@test length(fixed) == 2
497+
@test Tuple(fixed) === (4, 5)
498+
end
499+
471500
@testitem "PyPandasDataFrame" setup=[Setup] begin
472501
using Tables
473502
@test PyPandasDataFrame isa Type

0 commit comments

Comments
 (0)