Skip to content

Commit 63b2801

Browse files
author
Christopher Doris
committed
Merge remote-tracking branch 'origin/main' into v1
2 parents b81c389 + 875c34d commit 63b2801

File tree

8 files changed

+203
-94
lines changed

8 files changed

+203
-94
lines changed

docs/src/compat.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ functions to bridge a gap. We aim to keep these as minimal as possible.
99

1010
Whenever a Python exception is displayed by Julia, `sys.last_traceback` and friends are set. This allows the post-mortem debugger `pdb.pm()` to work. Disable by setting `PythonCall.CONFIG.auto_sys_last_traceback = false`.
1111

12+
## Julia standard library
13+
14+
Python objects can be serialised with the [`Serialization`](https://docs.julialang.org/en/v1/stdlib/Serialization/) stdlib.
15+
This uses [`pickle`](https://docs.python.org/3/library/pickle.html) library under the hood.
16+
You can opt into using [`dill`](https://pypi.org/project/dill/) instead by setting the environment variable `JULIA_PYTHONCALL_PICKLE="dill"`.
17+
1218
## Tabular data / Pandas
1319

1420
The abstract type [`PyTable`](@ref) is for wrapper types around Python tables, providing the

docs/src/releasenotes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Unreleased (v1)
44
* `PythonCall.GC` is now more like `Base.GC`: `enable(true)` replaces `enable()`, `enable(false)` replaces `disable()`, and `gc()` is added.
55

6+
## Unreleased
7+
* `Serialization.serialize` can use `dill` instead of `pickle` by setting the env var `JULIA_PYTHONCALL_PICKLE=dill`.
8+
* `datetime.timedelta` can now be converted to `Dates.Nanosecond`, `Microsecond`, `Millisecond` and `Second`. This behaviour was already documented.
9+
610
## 0.9.20 (2024-05-01)
711
* The IPython extension is now automatically loaded upon import if IPython is detected.
812
* JuliaCall now compatible with Julia 1.10.3.

src/Compat/serialization.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
#
33
# We use pickle to serialise Python objects to bytes.
44

5+
_pickle_module() = pyimport(get(ENV, "JULIA_PYTHONCALL_PICKLE", "pickle"))
6+
57
function serialize_py(s, x::Py)
68
if pyisnull(x)
79
serialize(s, nothing)
810
else
9-
b = pyimport("pickle").dumps(x)
11+
b = _pickle_module().dumps(x)
1012
serialize(s, pybytes_asvector(b))
1113
end
1214
end
@@ -16,7 +18,7 @@ function deserialize_py(s)
1618
if v === nothing
1719
pynew()
1820
else
19-
pyimport("pickle").loads(pybytes(v))
21+
_pickle_module().loads(pybytes(v))
2022
end
2123
end
2224

src/Convert/Convert.jl

Lines changed: 2 additions & 2 deletions
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

Lines changed: 26 additions & 21 deletions
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

Lines changed: 50 additions & 6 deletions
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

0 commit comments

Comments
 (0)