Skip to content

Commit 114cd40

Browse files
author
Christopher Doris
committed
adds PyTuple wrapper type
1 parent 71666ca commit 114cd40

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

src/API/exports.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export PyDict
117117
export PyIO
118118
export PyIterable
119119
export PyList
120+
export PyTuple
121+
export PyNTuple
122+
for n = 1:8
123+
@eval export $(Symbol(:Py, n, :Tuple))
124+
end
120125
export PyPandasDataFrame
121126
export PySet
122127
export PyTable

src/API/types.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,42 @@ 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]}([x])
186+
187+
Wraps the Python tuple `x` as a Julia wrapper parametrised by a tuple type `T`.
188+
189+
For example a `PyTuple{Tuple{Int,String}}` holds an `Int` and a `String`, a
190+
`PyTuple{Tuple{Int,Vararg{String}}}` holds and `Int` and any number of `String`s, and
191+
a `PyTuple{Tuple}` holds any number of anything.
192+
193+
Supports `length(t)`, indexing `t[i]`, iteration `for x in t`, `Tuple(t)`, `eltype(t)`
194+
just as for an ordinary `Tuple`.
195+
196+
For convenience, these aliases are also exported:
197+
- `PyNTuple{N,T}` for a tuple with `N` fields of the same type `T`, analogous to `NTuple{N,T}`
198+
- `Py0Tuple`, `Py1Tuple{T1}`, ..., `Py8Tuple{T1,...,T8}` for tuples of a particular length
199+
"""
200+
struct PyTuple{T<:Tuple}
201+
py::Py
202+
Base.@propagate_inbounds function PyTuple{T}(x = pytuple()) where {T<:Tuple}
203+
ans = new{T}(ispy(x) ? Py(x) : pytuple(x))
204+
@boundscheck (
205+
PythonCall.Wrap.check_length(ans) || error(
206+
"tuple is incorrect length for this PyTuple type, got len=$(pylen(ans.py))",
207+
)
208+
)
209+
ans
210+
end
211+
end
212+
213+
const PyNTuple{N,T} = PyTuple{NTuple{N,T}}
214+
215+
for n = 1:8
216+
Ts = [Symbol(:T, i) for i = 1:n]
217+
@eval $(Symbol(:Py, n, :Tuple)){$(Ts...)} = PyTuple{Tuple{$(Ts...)}}
218+
end
219+
184220
"""
185221
PyTable(x)
186222

src/Wrap/PyTuple.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
PyTuple(x = pytuple()) = PyTuple{Tuple}(x)
2+
3+
ispy(::PyTuple) = true
4+
Py(x::PyTuple) = x.py
5+
6+
@generated function static_length(::PyTuple{T}) where {T}
7+
try
8+
fieldcount(T)
9+
catch
10+
nothing
11+
end
12+
end
13+
14+
@generated function min_length(::PyTuple{T}) where {T}
15+
count(!Base.isvarargtype, T.parameters)
16+
end
17+
18+
function check_length(x::PyTuple)
19+
len = pylen(x.py)
20+
explen = PythonCall.Wrap.static_length(x)
21+
if explen === nothing
22+
minlen = PythonCall.Wrap.min_length(x)
23+
len minlen
24+
else
25+
len == explen
26+
end
27+
end
28+
29+
Base.IteratorSize(::Type{<:PyTuple}) = Base.HasLength()
30+
31+
Base.length(x::PyTuple{T}) where {T<:Tuple} =
32+
@something(static_length(x), max(min_length(x), Int(pylen(x.py))))
33+
34+
Base.IteratorEltype(::Type{<:PyTuple}) = Base.HasEltype()
35+
36+
Base.eltype(::Type{PyTuple{T}}) where {T<:Tuple} = eltype(T)
37+
38+
Base.checkbounds(::Type{Bool}, x::PyTuple, i::Integer) = 1 i length(x)
39+
40+
Base.checkbounds(x::PyTuple, i::Integer) =
41+
if !checkbounds(Bool, x, i)
42+
throw(BoundsError(x, i))
43+
end
44+
45+
Base.@propagate_inbounds function Base.getindex(x::PyTuple{T}, i::Integer) where {T<:Tuple}
46+
i = convert(Int, i)::Int
47+
@boundscheck checkbounds(x, i)
48+
E = fieldtype(T, i)
49+
return pyconvert(E, @py x[@jl(i - 1)])
50+
end
51+
52+
Base.@propagate_inbounds function Base.setindex!(
53+
x::PyTuple{T},
54+
v,
55+
i::Integer,
56+
) where {T<:Tuple}
57+
i = convert(Int, i)::Int
58+
@boundscheck checkbounds(x, i)
59+
E = fieldtype(T, i)
60+
v = convert(E, v)::E
61+
@py x[@jl(i - 1)] = v
62+
x
63+
end
64+
65+
function Base.iterate(x::PyTuple{T}, ni = (length(x), 1)) where {T<:Tuple}
66+
n, i = ni
67+
if i > @something(static_length(x), n)
68+
nothing
69+
else
70+
(x[i], (n, i + 1))
71+
end
72+
end
73+
74+
function Base.Tuple(x::PyTuple{T}) where {T<:Tuple}
75+
n = static_length(x)
76+
if n === nothing
77+
ntuple(i -> x[i], length(x))::T
78+
else
79+
ntuple(i -> x[i], Val(n))::T
80+
end
81+
end
82+
83+
# Conversion rule for builtins:tuple -> PyTuple
84+
function pyconvert_rule_tuple(
85+
::Type{T},
86+
x::Py,
87+
::Type{T1} = Utils._type_ub(T),
88+
) where {T<:PyTuple,T1}
89+
ans = @inbounds T1(x)
90+
if check_length(ans)
91+
pyconvert_return(ans)
92+
else
93+
pyconvert_unconverted()
94+
end
95+
end
96+
97+
function Base.show(io::IO, mime::MIME"text/plain", x::PyTuple)
98+
if !(get(io, :typeinfo, Any) <: PyTuple)
99+
print(io, "PyTuple: ")
100+
end
101+
show(io, mime, Tuple(x))
102+
nothing
103+
end

src/Wrap/Wrap.jl

Lines changed: 3 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, PyTuple, PyPandasDataFrame, PySet, PyTable
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")
@@ -69,6 +70,7 @@ function __init__()
6970
)
7071

7172
priority = PYCONVERT_PRIORITY_NORMAL
73+
pyconvert_add_rule("builtins:tuple", PyTuple, pyconvert_rule_tuple, priority)
7274
pyconvert_add_rule("<arraystruct>", Array, pyconvert_rule_array, priority)
7375
pyconvert_add_rule("<arrayinterface>", Array, pyconvert_rule_array, priority)
7476
pyconvert_add_rule("<array>", Array, pyconvert_rule_array, priority)

0 commit comments

Comments
 (0)