Skip to content

Commit 5fbcd2e

Browse files
author
Christopher Doris
committed
add conversion for named tuples
1 parent 98aa242 commit 5fbcd2e

File tree

4 files changed

+44
-0
lines changed

4 files changed

+44
-0
lines changed

docs/src/releasenotes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Release Notes
22

33
## Unreleased
4+
* Python named tuples can be converted to Julia named tuples. So can Python iterables, if
5+
the target `NamedTuple` type specifies field names.
46
* Bug fixes.
57

68
## 0.9.5 (2022-08-19)

src/abstract/collection.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,29 @@ function pyconvert_rule_iterable(::Type{R}, x::Py, ::Type{Pair{K0,V0}}=Utils._ty
169169
V2 = Utils._promote_type_bounded(V0, typeof(v), V1)
170170
return pyconvert_return(Pair{K2,V2}(k, v))
171171
end
172+
173+
# NamedTuple
174+
175+
_nt_names_types(::Type) = nothing
176+
_nt_names_types(::Type{NamedTuple}) = (nothing, nothing)
177+
_nt_names_types(::Type{NamedTuple{names}}) where {names} = (names, nothing)
178+
_nt_names_types(::Type{NamedTuple{names,types} where {names}}) where {types} = (nothing, types)
179+
_nt_names_types(::Type{NamedTuple{names,types}}) where {names,types} = (names, types)
180+
181+
function pyconvert_rule_iterable(::Type{R}, x::Py) where {R<:NamedTuple}
182+
# this is actually strict and only converts python named tuples (i.e. tuples with a
183+
# _fields attribute) where the field names match those from R (if specified).
184+
names_types = _nt_names_types(R)
185+
names_types === nothing && return pyconvert_unconverted()
186+
names, types = names_types
187+
PythonCall.pyistuple(x) || return pyconvert_unconverted()
188+
names2_ = pygetattr(x, "_fields", pybuiltins.None)
189+
names2 = @pyconvert(names === nothing ? Tuple{Vararg{Symbol}} : typeof(names), names2_)
190+
pydel!(names2_)
191+
names === nothing || names === names2 || return pyconvert_unconverted()
192+
types2 = types === nothing ? NTuple{length(names2),Any} : types
193+
vals = @pyconvert(types2, x)
194+
length(vals) == length(names2) || return pyconvert_unconverted()
195+
types3 = types === nothing ? typeof(vals) : types
196+
return pyconvert_return(NamedTuple{names2,types3}(vals))
197+
end

src/convert.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ function init_pyconvert()
415415
pyconvert_add_rule("_io:_IOBase", PyIO, pyconvert_rule_io, priority)
416416
pyconvert_add_rule("pandas.core.frame:DataFrame", PyPandasDataFrame, pyconvert_rule_pandasdataframe, priority)
417417
pyconvert_add_rule("pandas.core.arrays.base:ExtensionArray", PyList, pyconvert_rule_sequence, priority)
418+
pyconvert_add_rule("builtins:tuple", NamedTuple, pyconvert_rule_iterable, priority)
418419
pyconvert_add_rule("builtins:tuple", Tuple, pyconvert_rule_iterable, priority)
419420
pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority)
420421
pyconvert_add_rule("datetime:date", Date, pyconvert_rule_date, priority)

test/convert.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,21 @@ end
154154
@test x2 === ("foo" => missing)
155155
end
156156

157+
@testitem "named tuple → NamedTuple" begin
158+
NT = pyimport("collections" => "namedtuple")
159+
t1 = pyconvert(NamedTuple, NT("NT", "x y")(1, 2))
160+
@test t1 === (x=1, y=2)
161+
@test_throws Exception pyconvert(NamedTuple, (2, 3))
162+
t2 = pyconvert(NamedTuple{(:x, :y)}, NT("NT", "x y")(3, 4))
163+
@test t2 === (x=3, y=4)
164+
@test_throws Exception pyconvert(NamedTuple{(:y, :x)}, NT("NT", "x y")(3, 4))
165+
t3 = pyconvert(NamedTuple{names,Tuple{Int,Int}} where {names}, NT("NT", "x y")(4, 5))
166+
@test t3 === (x=4, y=5)
167+
@test_throws Exception pyconvert(NamedTuple{names,Tuple{Int,Int}} where {names}, (5, 6))
168+
t4 = pyconvert(NamedTuple{(:x, :y),Tuple{Int,Int}}, NT("NT", "x y")(6, 7))
169+
@test t4 === (x=6, y=7)
170+
end
171+
157172
@testitem "mapping → Dict" begin
158173
x1 = pyconvert(Dict, pydict(["a"=>1, "b"=>2]))
159174
@test x1 isa Dict{String, Int}

0 commit comments

Comments
 (0)