Skip to content

Commit 875c34d

Browse files
authored
Timedelta conversion (#499)
* add rules for datetime.timedelta * tests for timedelta conversion * document new timedelta conversion rules --------- Co-authored-by: Christopher Doris <github.com/cjdoris>
1 parent aca2701 commit 875c34d

File tree

5 files changed

+121
-29
lines changed

5 files changed

+121
-29
lines changed

docs/src/releasenotes.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
* `Serialization.serialize` can use `dill` instead of `pickle` by setting the env var `JULIA_PYTHONCALL_PICKLE=dill`.
5+
* `datetime.timedelta` can now be converted to `Dates.Nanosecond`, `Microsecond`, `Millisecond` and `Second`. This behaviour was already documented.
56

67
## 0.9.20 (2024-05-01)
78
* The IPython extension is now automatically loaded upon import if IPython is detected.

src/Convert/Convert.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module Convert
77

88
using ..Core
99
using ..Core: C, Utils, @autopy, getptr, incref, pynew, PyNULL, pyisnull, pydel!, pyisint, iserrset_ambig, pyisnone, pyisTrue, pyisFalse, pyfloat_asdouble, pycomplex_ascomplex, pyisstr, pystr_asstring, pyisbytes, pybytes_asvector, pybytes_asUTF8string, pyisfloat, pyisrange, pytuple_getitem, unsafe_pynext, pyistuple, pydatetimetype, pytime_isaware, pydatetime_isaware, _base_pydatetime, _base_datetime, errmatches, errclear, errset, pyiscomplex, pythrow, pybool_asbool
10-
using Dates: Date, Time, DateTime, Millisecond
10+
using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond
1111

1212
import ..Core: pyconvert
1313

@@ -18,7 +18,7 @@ include("numpy.jl")
1818
include("pandas.jl")
1919

2020
function __init__()
21-
C.with_gil() do
21+
C.with_gil() do
2222
init_pyconvert()
2323
init_ctypes()
2424
init_numpy()

src/Convert/pyconvert.jl

+26-21
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
end
88

99
struct PyConvertRule
10-
type :: Type
11-
func :: Function
12-
priority :: PyConvertPriority
10+
type::Type
11+
func::Function
12+
priority::PyConvertPriority
1313
end
1414

15-
const PYCONVERT_RULES = Dict{String, Vector{PyConvertRule}}()
15+
const PYCONVERT_RULES = Dict{String,Vector{PyConvertRule}}()
1616
const PYCONVERT_EXTRATYPES = Py[]
1717

1818
"""
@@ -201,7 +201,7 @@ function _pyconvert_get_rules(pytype::Py)
201201
# check the original MRO is preserved
202202
omro_ = filter(t -> pyisin(t, omro), mro)
203203
@assert length(omro) == length(omro_)
204-
@assert all(pyis(x,y) for (x,y) in zip(omro, omro_))
204+
@assert all(pyis(x, y) for (x, y) in zip(omro, omro_))
205205

206206
# get the names of the types in the MRO of pytype
207207
xmro = [String[pyconvert_typename(t)] for t in mro]
@@ -240,22 +240,23 @@ function _pyconvert_get_rules(pytype::Py)
240240
rules = PyConvertRule[rule for tname in mro for rule in get!(Vector{PyConvertRule}, PYCONVERT_RULES, tname)]
241241

242242
# order the rules by priority, then by original order
243-
order = sort(axes(rules, 1), by = i -> (rules[i].priority, -i), rev = true)
243+
order = sort(axes(rules, 1), by=i -> (rules[i].priority, -i), rev=true)
244244
rules = rules[order]
245245

246-
@debug "pyconvert" pytype mro=join(mro, " ")
246+
@debug "pyconvert" pytype mro = join(mro, " ")
247247
return rules
248248
end
249249

250250
const PYCONVERT_PREFERRED_TYPE = Dict{Py,Type}()
251251

252-
pyconvert_preferred_type(pytype::Py) = get!(PYCONVERT_PREFERRED_TYPE, pytype) do
253-
if pyissubclass(pytype, pybuiltins.int)
254-
Union{Int,BigInt}
255-
else
256-
_pyconvert_get_rules(pytype)[1].type
252+
pyconvert_preferred_type(pytype::Py) =
253+
get!(PYCONVERT_PREFERRED_TYPE, pytype) do
254+
if pyissubclass(pytype, pybuiltins.int)
255+
Union{Int,BigInt}
256+
else
257+
_pyconvert_get_rules(pytype)[1].type
258+
end
257259
end
258-
end
259260

260261
function pyconvert_get_rules(type::Type, pytype::Py)
261262
@nospecialize type
@@ -281,15 +282,15 @@ end
281282

282283
pyconvert_fix(::Type{T}, func) where {T} = x -> func(T, x)
283284

284-
const PYCONVERT_RULES_CACHE = Dict{Type, Dict{C.PyPtr, Vector{Function}}}()
285+
const PYCONVERT_RULES_CACHE = Dict{Type,Dict{C.PyPtr,Vector{Function}}}()
285286

286-
@generated pyconvert_rules_cache(::Type{T}) where {T} = get!(Dict{C.PyPtr, Vector{Function}}, PYCONVERT_RULES_CACHE, T)
287+
@generated pyconvert_rules_cache(::Type{T}) where {T} = get!(Dict{C.PyPtr,Vector{Function}}, PYCONVERT_RULES_CACHE, T)
287288

288289
function pyconvert_rule_fast(::Type{T}, x::Py) where {T}
289290
if T isa Union
290-
a = pyconvert_rule_fast(T.a, x) :: pyconvert_returntype(T.a)
291+
a = pyconvert_rule_fast(T.a, x)::pyconvert_returntype(T.a)
291292
pyconvert_isunconverted(a) || return a
292-
b = pyconvert_rule_fast(T.b, x) :: pyconvert_returntype(T.b)
293+
b = pyconvert_rule_fast(T.b, x)::pyconvert_returntype(T.b)
293294
pyconvert_isunconverted(b) || return b
294295
elseif (T == Nothing) | (T == Missing)
295296
pyisnone(x) && return pyconvert_return(T())
@@ -318,7 +319,7 @@ function pytryconvert(::Type{T}, x_) where {T}
318319

319320
# We can optimize the conversion for some types by overloading pytryconvert_fast.
320321
# It MUST give the same results as via the slower route using rules.
321-
ans1 = pyconvert_rule_fast(T, x) :: pyconvert_returntype(T)
322+
ans1 = pyconvert_rule_fast(T, x)::pyconvert_returntype(T)
322323
pyconvert_isunconverted(ans1) || return ans1
323324

324325
# get rules from the cache
@@ -334,7 +335,7 @@ function pytryconvert(::Type{T}, x_) where {T}
334335

335336
# apply the rules
336337
for rule in rules
337-
ans2 = rule(x) :: pyconvert_returntype(T)
338+
ans2 = rule(x)::pyconvert_returntype(T)
338339
pyconvert_isunconverted(ans2) || return ans2
339340
end
340341

@@ -386,8 +387,8 @@ pyconvertarg(::Type{T}, x, name) where {T} = @autopy x @pyconvert T x_ begin
386387
end
387388

388389
function init_pyconvert()
389-
push!(PYCONVERT_EXTRATYPES, pyimport("io"=>"IOBase"))
390-
push!(PYCONVERT_EXTRATYPES, pyimport("numbers"=>("Number", "Complex", "Real", "Rational", "Integral"))...)
390+
push!(PYCONVERT_EXTRATYPES, pyimport("io" => "IOBase"))
391+
push!(PYCONVERT_EXTRATYPES, pyimport("numbers" => ("Number", "Complex", "Real", "Rational", "Integral"))...)
391392
push!(PYCONVERT_EXTRATYPES, pyimport("collections.abc" => ("Iterable", "Sequence", "Set", "Mapping"))...)
392393

393394
priority = PYCONVERT_PRIORITY_CANONICAL
@@ -405,6 +406,7 @@ function init_pyconvert()
405406
pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority)
406407
pyconvert_add_rule("datetime:date", Date, pyconvert_rule_date, priority)
407408
pyconvert_add_rule("datetime:time", Time, pyconvert_rule_time, priority)
409+
pyconvert_add_rule("datetime:timedelta", Microsecond, pyconvert_rule_timedelta, priority)
408410
pyconvert_add_rule("builtins:BaseException", PyException, pyconvert_rule_exception, priority)
409411

410412
priority = PYCONVERT_PRIORITY_NORMAL
@@ -428,6 +430,9 @@ function init_pyconvert()
428430
pyconvert_add_rule("collections.abc:Sequence", Tuple, pyconvert_rule_iterable, priority)
429431
pyconvert_add_rule("collections.abc:Set", Set, pyconvert_rule_iterable, priority)
430432
pyconvert_add_rule("collections.abc:Mapping", Dict, pyconvert_rule_mapping, priority)
433+
pyconvert_add_rule("datetime:timedelta", Millisecond, pyconvert_rule_timedelta, priority)
434+
pyconvert_add_rule("datetime:timedelta", Second, pyconvert_rule_timedelta, priority)
435+
pyconvert_add_rule("datetime:timedelta", Nanosecond, pyconvert_rule_timedelta, priority)
431436

432437
priority = PYCONVERT_PRIORITY_FALLBACK
433438
pyconvert_add_rule("builtins:object", Py, pyconvert_rule_object, priority)

src/Convert/rules.jl

+50-6
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function pyconvert_rule_range(::Type{R}, x::Py, ::Type{StepRange{T0,S0}}=Utils._
133133
a′, c′ = promote(a, c - oftype(c, sign(b)))
134134
T2 = Utils._promote_type_bounded(T0, typeof(a′), typeof(c′), T1)
135135
S2 = Utils._promote_type_bounded(S0, typeof(c′), S1)
136-
pyconvert_return(StepRange{T2, S2}(a′, b, c′))
136+
pyconvert_return(StepRange{T2,S2}(a′, b, c′))
137137
end
138138

139139
function pyconvert_rule_range(::Type{R}, x::Py, ::Type{UnitRange{T0}}=Utils._type_lb(R), ::Type{UnitRange{T1}}=Utils._type_ub(R)) where {R<:UnitRange,T0,T1}
@@ -261,7 +261,7 @@ function pyconvert_rule_iterable(::Type{T}, xs::Py) where {T<:Tuple}
261261
zs = Any[]
262262
for x in xs
263263
if length(zs) < length(ts)
264-
t = ts[length(zs) + 1]
264+
t = ts[length(zs)+1]
265265
elseif isvararg
266266
t = vartype
267267
else
@@ -282,7 +282,7 @@ for N in 0:16
282282
n = pylen(xs)
283283
n == $N || return pyconvert_unconverted()
284284
$((
285-
:($z = @pyconvert($T, pytuple_getitem(xs, $(i-1))))
285+
:($z = @pyconvert($T, pytuple_getitem(xs, $(i - 1))))
286286
for (i, T, z) in zip(1:N, Ts, zs)
287287
)...)
288288
return pyconvert_return(($(zs...),))
@@ -293,12 +293,12 @@ for N in 0:16
293293
n = pylen(xs)
294294
n $N || return pyconvert_unconverted()
295295
$((
296-
:($z = @pyconvert($T, pytuple_getitem(xs, $(i-1))))
296+
:($z = @pyconvert($T, pytuple_getitem(xs, $(i - 1))))
297297
for (i, T, z) in zip(1:N, Ts, zs)
298298
)...)
299299
vs = V[]
300-
for i in $(N+1):n
301-
v = @pyconvert(V, pytuple_getitem(xs, i-1))
300+
for i in $(N + 1):n
301+
v = @pyconvert(V, pytuple_getitem(xs, i - 1))
302302
push!(vs, v)
303303
end
304304
return pyconvert_return(($(zs...), vs...))
@@ -395,3 +395,47 @@ function pyconvert_rule_datetime(::Type{DateTime}, x::Py)
395395
iszero(mod(microseconds, 1000)) || return pyconvert_unconverted()
396396
return pyconvert_return(_base_datetime + Millisecond(div(microseconds, 1000) + 1000 * (seconds + 60 * 60 * 24 * days)))
397397
end
398+
399+
function pyconvert_rule_timedelta(::Type{Nanosecond}, x::Py)
400+
days = pyconvert(Int, x.days)
401+
if abs(days) 106751
402+
# overflow
403+
return pyconvert_unconverted()
404+
end
405+
seconds = pyconvert(Int, x.seconds)
406+
microseconds = pyconvert(Int, x.microseconds)
407+
return Nanosecond(((days * 3600 * 24 + seconds) * 1000000 + microseconds) * 1000)
408+
end
409+
410+
function pyconvert_rule_timedelta(::Type{Microsecond}, x::Py)
411+
days = pyconvert(Int, x.days)
412+
if abs(days) 106751990
413+
# overflow
414+
return pyconvert_unconverted()
415+
end
416+
seconds = pyconvert(Int, x.seconds)
417+
microseconds = pyconvert(Int, x.microseconds)
418+
return Microsecond((days * 3600 * 24 + seconds) * 1000000 + microseconds)
419+
end
420+
421+
function pyconvert_rule_timedelta(::Type{Millisecond}, x::Py)
422+
days = pyconvert(Int, x.days)
423+
seconds = pyconvert(Int, x.seconds)
424+
microseconds = pyconvert(Int, x.microseconds)
425+
if mod(microseconds, 1000) != 0
426+
# inexact
427+
return pyconvert_unconverted()
428+
end
429+
return Millisecond((days * 3600 * 24 + seconds) * 1000 + div(microseconds, 1000))
430+
end
431+
432+
function pyconvert_rule_timedelta(::Type{Second}, x::Py)
433+
days = pyconvert(Int, x.days)
434+
seconds = pyconvert(Int, x.seconds)
435+
microseconds = pyconvert(Int, x.microseconds)
436+
if microseconds != 0
437+
# inexact
438+
return pyconvert_unconverted()
439+
end
440+
return Second(days * 3600 * 24 + seconds)
441+
end

test/Convert.jl

+42
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,48 @@ end
223223
@test x1 === DateTime(2001, 2, 3, 4, 5, 6, 7)
224224
end
225225

226+
@testitem "timedelta → Nanosecond" begin
227+
using Dates
228+
td = pyimport("datetime").timedelta
229+
@testset for x in [-1_000_000_000, -1_000_000, -1_000, -1, 0, 1, 1_000, 1_000_000, 1_000_000_000]
230+
y = pyconvert(Nanosecond, td(microseconds=x))
231+
@test y === Nanosecond(x * 1000)
232+
end
233+
@test_throws Exception pyconvert(Nanosecond, td(days=200_000))
234+
@test_throws Exception pyconvert(Nanosecond, td(days=-200_000))
235+
end
236+
237+
@testitem "timedelta → Microsecond" begin
238+
using Dates
239+
td = pyimport("datetime").timedelta
240+
@testset for x in [-1_000_000_000, -1_000_000, -1_000, -1, 0, 1, 1_000, 1_000_000, 1_000_000_000]
241+
y = pyconvert(Microsecond, td(microseconds=x))
242+
@test y === Microsecond(x)
243+
end
244+
@test_throws Exception pyconvert(Microsecond, td(days=200_000_000))
245+
@test_throws Exception pyconvert(Microsecond, td(days=-200_000_000))
246+
end
247+
248+
@testitem "timedelta → Millisecond" begin
249+
using Dates
250+
td = pyimport("datetime").timedelta
251+
@testset for x in [-1_000_000_000, -1_000_000, -1_000, -1, 0, 1, 1_000, 1_000_000, 1_000_000_000]
252+
y = pyconvert(Millisecond, td(microseconds=x*1000))
253+
@test y === Millisecond(x)
254+
end
255+
@test_throws Exception pyconvert(Millisecond, td(microseconds=1))
256+
end
257+
258+
@testitem "timedelta → Second" begin
259+
using Dates
260+
td = pyimport("datetime").timedelta
261+
@testset for x in [-1_000_000_000, -1_000_000, -1_000, -1, 0, 1, 1_000, 1_000_000, 1_000_000_000]
262+
y = pyconvert(Second, td(seconds=x))
263+
@test y === Second(x)
264+
end
265+
@test_throws Exception pyconvert(Second, td(microseconds=1000))
266+
end
267+
226268
@testitem "pyconvert_add_rule (#364)" begin
227269
id = string(rand(UInt128), base=16)
228270
pyexec("""

0 commit comments

Comments
 (0)