Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/src/pythoncall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Apart from a few fundamental immutable types, conversion from Python to Julia `A

```@docs
PyList
PyTuple
PySet
PyDict
PyIterable
Expand Down
1 change: 1 addition & 0 deletions docs/src/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Adds methods `Py(::AbstractString)`, `Py(::AbstractChar)` (previously only builtin string and char types were allowed).
* Adds methods `Py(::Integer)`, `Py(::Rational{<:Integer})`, `Py(::AbstractRange{<:Integer})` (previously only builtin integer types were allowed).
* Adds method `pydict(::Pair...)` to construct a python `dict` from `Pair`s, similar to `Dict`.
* Added `PyTuple` wrapper for Python tuples with typed indexing.
* Bug fixes.
* Internal: switch from Requires.jl to package extensions.

Expand Down
5 changes: 5 additions & 0 deletions src/API/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ export PyDict
export PyIO
export PyIterable
export PyList
export PyTuple
export PyNTuple
for n = 0:8
@eval export $(Symbol(:Py, n, :Tuple))
end
export PyPandasDataFrame
export PySet
export PyTable
Expand Down
38 changes: 38 additions & 0 deletions src/API/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,44 @@ struct PyList{T} <: AbstractVector{T}
PyList{T}(x = pylist()) where {T} = new{T}(ispy(x) ? Py(x) : pylist(x))
end

"""
PyTuple{[T<:Tuple]}([x])

Wraps the Python tuple `x` as a Julia wrapper parametrised by a tuple type `T`.

For example a `PyTuple{Tuple{Int,String}}` holds an `Int` and a `String`, a
`PyTuple{Tuple{Int,Vararg{String}}}` holds and `Int` and any number of `String`s, and
a `PyTuple{Tuple}` holds any number of anything.

Supports `length(t)`, indexing `t[i]`, iteration `for x in t`, `Tuple(t)`, `eltype(t)`
just as for an ordinary `Tuple`.

For convenience, these aliases are also exported:
- `PyNTuple{N,T}` for a tuple with `N` fields of the same type `T`, analogous to `NTuple{N,T}`
- `Py0Tuple`, `Py1Tuple{T1}`, ..., `Py8Tuple{T1,...,T8}` for tuples of a particular length
"""
struct PyTuple{T<:Tuple}
py::Py
Base.@propagate_inbounds function PyTuple{T}(x = pytuple()) where {T<:Tuple}
ans = new{T}(ispy(x) ? Py(x) : pytuple(x))
@boundscheck (
PythonCall.Wrap.check_length(ans) || error(
"tuple is incorrect length for this PyTuple type, got len=$(pylen(ans.py))",
)
)
ans
end
end

const PyNTuple{N,T} = PyTuple{NTuple{N,T}}

const Py0Tuple = PyTuple{Tuple{}}
for n = 1:8
Ts = [Symbol(:T, i) for i = 1:n]
name = Symbol(:Py, n, :Tuple)
@eval $name{$(Ts...)} = PyTuple{Tuple{$(Ts...)}}
end

"""
PyTable(x)

Expand Down
103 changes: 103 additions & 0 deletions src/Wrap/PyTuple.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
PyTuple(x = pytuple()) = PyTuple{Tuple}(x)

ispy(::PyTuple) = true
Py(x::PyTuple) = x.py

@generated function static_length(::PyTuple{T}) where {T}
try
fieldcount(T)
catch
nothing
end
end

@generated function min_length(::PyTuple{T}) where {T}
count(!Base.isvarargtype, T.parameters)
end

function check_length(x::PyTuple)
len = pylen(x.py)
explen = PythonCall.Wrap.static_length(x)
if explen === nothing
minlen = PythonCall.Wrap.min_length(x)
len ≥ minlen
else
len == explen
end
end

Base.IteratorSize(::Type{<:PyTuple}) = Base.HasLength()

Base.length(x::PyTuple{T}) where {T<:Tuple} =
@something(static_length(x), max(min_length(x), Int(pylen(x.py))))

Base.IteratorEltype(::Type{<:PyTuple}) = Base.HasEltype()

Base.eltype(::Type{PyTuple{T}}) where {T<:Tuple} = eltype(T)

Base.checkbounds(::Type{Bool}, x::PyTuple, i::Integer) = 1 ≤ i ≤ length(x)

Base.checkbounds(x::PyTuple, i::Integer) =
if !checkbounds(Bool, x, i)
throw(BoundsError(x, i))
end

Base.@propagate_inbounds function Base.getindex(x::PyTuple{T}, i::Integer) where {T<:Tuple}
i = convert(Int, i)::Int
@boundscheck checkbounds(x, i)
E = fieldtype(T, i)
return pyconvert(E, @py x[@jl(i - 1)])
end

Base.@propagate_inbounds function Base.setindex!(
x::PyTuple{T},
v,
i::Integer,
) where {T<:Tuple}
i = convert(Int, i)::Int
@boundscheck checkbounds(x, i)
E = fieldtype(T, i)
v = convert(E, v)::E
@py x[@jl(i - 1)] = v
x
end

function Base.iterate(x::PyTuple{T}, ni = (length(x), 1)) where {T<:Tuple}
n, i = ni
if i > @something(static_length(x), n)
nothing
else
(x[i], (n, i + 1))
end
end

function Base.Tuple(x::PyTuple{T}) where {T<:Tuple}
n = static_length(x)
if n === nothing
ntuple(i -> x[i], length(x))::T
else
ntuple(i -> x[i], Val(n))::T
end
end

# Conversion rule for Sequence -> PyTuple
function pyconvert_rule_sequence(
::Type{T},
x::Py,
::Type{T1} = Utils._type_ub(T),
) where {T<:PyTuple,T1}
ans = @inbounds T1(x)
if check_length(ans)
pyconvert_return(ans)
else
pyconvert_unconverted()
end
end

function Base.show(io::IO, mime::MIME"text/plain", x::PyTuple)
if !(get(io, :typeinfo, Any) <: PyTuple)
print(io, "PyTuple: ")
end
show(io, mime, Tuple(x))
nothing
end
9 changes: 8 additions & 1 deletion src/Wrap/Wrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using ..Convert
using ..PyMacro

import ..PythonCall:
PyArray, PyDict, PyIO, PyIterable, PyList, PyPandasDataFrame, PySet, PyTable
PyArray, PyDict, PyIO, PyIterable, PyList, PyTuple, PyPandasDataFrame, PySet, PyTable

using Base: @propagate_inbounds
using Tables: Tables
Expand All @@ -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")
Expand Down Expand Up @@ -69,6 +70,12 @@ function __init__()
)

priority = PYCONVERT_PRIORITY_NORMAL
pyconvert_add_rule(
"collections.abc:Sequence",
PyTuple,
pyconvert_rule_sequence,
priority,
)
pyconvert_add_rule("<arraystruct>", Array, pyconvert_rule_array, priority)
pyconvert_add_rule("<arrayinterface>", Array, pyconvert_rule_array, priority)
pyconvert_add_rule("<array>", Array, pyconvert_rule_array, priority)
Expand Down
9 changes: 9 additions & 0 deletions test/Convert.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ end
@test x2 == [1, 2, 3]
end

@testitem "sequence → PyTuple" begin
x1 = pyconvert(PyTuple, pylist([1, "foo", "B"]))
@test x1 isa PyTuple{Tuple}
@test isequal(Tuple(x1), (1, "foo", "B"))
x2 = pyconvert(PyTuple{Tuple{Int,Symbol,Char}}, pylist([1, "foo", "B"]))
@test x2 isa PyTuple{Tuple{Int,Symbol,Char}}
@test isequal(Tuple(x2), (1, :foo, 'B'))
end

@testitem "set → PySet" begin
x1 = pyconvert(PySet, pyset([1, 2, 3]))
@test x1 isa PySet{Any}
Expand Down
32 changes: 32 additions & 0 deletions test/Wrap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,35 @@ end
@test PyTable isa Type
@test_throws Exception PyTable(0)
end

@testitem "PyTuple" begin
x = pytuple((1, "a"))
y = PyTuple(x)
z = PyTuple{Tuple{Int,String}}(x)
@testset "construct" begin
@test y isa PyTuple{Tuple}
@test z isa PyTuple{Tuple{Int,String}}
@test PythonCall.ispy(y)
@test PythonCall.ispy(z)
@test Py(y) === x
@test Py(z) === x
end
@testset "length" begin
@test length(y) == 2
@test length(z) == 2
v = PyTuple{Tuple{Int,Vararg{String}}}(pytuple((1, "a", "b")))
@test length(v) == 3
end
@testset "getindex" begin
@test_throws BoundsError y[0]
@test y[1] === 1
@test y[2] == "a"
@test z[1] === 1
@test z[2] == "a"
@test_throws BoundsError y[3]
end
@testset "Tuple" begin
@test Tuple(y) == (1, "a")
@test Tuple(z) == (1, "a")
end
end
Loading