From 093bdcb09b38f60f9b277fd96978bb5628fc933e Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 21 Feb 2022 16:26:28 +0100 Subject: [PATCH 01/20] add support for `@py from import` --- src/py_macro.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/py_macro.jl b/src/py_macro.jl index abf3de40..a4a5c7ce 100644 --- a/src/py_macro.jl +++ b/src/py_macro.jl @@ -793,10 +793,26 @@ For example: - `x.foo` is translated to `pygetattr(x, "foo")` Compound statements such as `begin`, `if`, `while` and `for` are supported. +Import statements are supported, e.g. +- `import foo, bar` +- `from os.path import join as py_joinpath, exists` See the online documentation for more details. """ macro py(ex) esc(py_macro(ex, __module__, __source__)) end + +macro py(keyword, modulename, ex) + keyword == :from || return :( nothing ) + + d = Dict(isa(a.args[1], Symbol) ? a.args[1] => a.args[1] : a.args[1].args[1] => a.args[2] for a in ex.args) + vars = Expr(:tuple, values(d)...) + imports = Tuple(keys(d)) + + esc(quote + $vars = pyimport($(string(modulename)) => $(string.(imports))) + end) +end + export @py From 5740b592cc6f59c237b27d3d349bd78852db37d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Sun, 16 Jun 2024 23:54:41 +0200 Subject: [PATCH 02/20] support timedelta, timedelta64, datetime64 and respective conversions --- src/Convert/Convert.jl | 1 + src/Convert/numpy.jl | 74 ++++++++++++++++++++++++++++++++++++++++ src/Convert/pyconvert.jl | 6 ++++ src/Convert/rules.jl | 13 +++++++ src/Core/Core.jl | 2 ++ src/Core/Py.jl | 1 + src/Core/builtins.jl | 18 ++++++++++ 7 files changed, 115 insertions(+) diff --git a/src/Convert/Convert.jl b/src/Convert/Convert.jl index 0cccbf58..20c70f80 100644 --- a/src/Convert/Convert.jl +++ b/src/Convert/Convert.jl @@ -8,6 +8,7 @@ module Convert using ..Core 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 using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond +using Dates: Year, Month, Day, Hour, Minute, Week, Period, CompoundPeriod, canonicalize import ..Core: pyconvert diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index f9dadbd4..2634ac0d 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -27,6 +27,70 @@ const NUMPY_SIMPLE_TYPES = [ ("complex128", ComplexF64), ] +function pydatetime64( + _year::Int=0, _month::Int=1, _day::Int=1, _hour::Int=0, _minute::Int=0,_second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0; + year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, + millisecond::Int=_millisecond, microsecond::Int=_microsecond, nanosecond::Int=_nanosecond +) + pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + pytimedelta64(;millisecond, microsecond, nanosecond) +end +function pydatetime64(@nospecialize(x::T)) where T <: Period + T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || + error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.") + args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) + pydatetime64(x.value .* args...) +end +function pydatetime64(x::CompoundPeriod) + x = canonicalize(x) + isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64.(x.periods)) +end +export pydatetime64 + +function pytimedelta64( + _year::Int=0, _month::Int=0, _day::Int=0, _hour::Int=0, _minute::Int=0, _second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0, _week::Int=0; + year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, nanosecond::Int=_nanosecond, week::Int=_week) + pytimedelta64(sum(( + Year(year), Month(month), # you cannot mix year or month with any of the below units in python, the error will be thrown by `pytimedelta64(::CompoundPeriod)` + Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond)) + )) +end +function pytimedelta64(@nospecialize(x::T)) where T <: Period + index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T)) + unit = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "")[index] + pyimport("numpy").timedelta64(x.value, unit) +end +function pytimedelta64(x::CompoundPeriod) + x = canonicalize(x) + isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods)) +end +export pytimedelta64 + +function pyconvert_rule_datetime64(::Type{DateTime}, x::Py) + unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) + # strangely, datetime_data does not return the value correctly + # so we retrieve the value from the byte representation + value = reinterpret(Int64, pyconvert(Vector, x))[1] + units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") + types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) + T = types[findfirst(==(unit), units)] + pyconvert_return(DateTime(_base_datetime) + T(value)) +end + +function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py) + unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) + # strangely, datetime_data does not return the value correctly + # so we retrieve the value from the byte representation + value = reinterpret(Int64, pyconvert(Vector, x))[1] + units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") + types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) + T = types[findfirst(==(unit), units)] + pyconvert_return(CompoundPeriod(T(value)) |> canonicalize) +end + +function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period + pyconvert_return(convert(T, pyconvert_rule_timedelta64(CompoundPeriod, x))) +end + function init_numpy() for (t, T) in NUMPY_SIMPLE_TYPES isbool = occursin("bool", t) @@ -52,4 +116,14 @@ function init_numpy() iscomplex && pyconvert_add_rule(name, Complex, rule) isnumber && pyconvert_add_rule(name, Number, rule) end + + priority = PYCONVERT_PRIORITY_ARRAY + pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority) + for T in (CompoundPeriod, Year, Month, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, Week) + pyconvert_add_rule("numpy:timedelta64", T, pyconvert_rule_timedelta64, priority) + end + + priority = PYCONVERT_PRIORITY_CANONICAL + pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority) + pyconvert_add_rule("numpy:timedelta64", Nanosecond, pyconvert_rule_timedelta, priority) end diff --git a/src/Convert/pyconvert.jl b/src/Convert/pyconvert.jl index 9963940f..c63f5ad4 100644 --- a/src/Convert/pyconvert.jl +++ b/src/Convert/pyconvert.jl @@ -391,6 +391,12 @@ function init_pyconvert() push!(PYCONVERT_EXTRATYPES, pyimport("numbers" => ("Number", "Complex", "Real", "Rational", "Integral"))...) push!(PYCONVERT_EXTRATYPES, pyimport("collections.abc" => ("Iterable", "Sequence", "Set", "Mapping"))...) + priority = PYCONVERT_PRIORITY_ARRAY + pyconvert_add_rule("datetime:datetime", DateTime, pyconvert_rule_datetime, priority) + for T in (Millisecond, Second, Nanosecond, Day, Hour, Minute, Second, Millisecond, Week, CompoundPeriod) + pyconvert_add_rule("datetime:timedelta", T, pyconvert_rule_timedelta, priority) + end + priority = PYCONVERT_PRIORITY_CANONICAL pyconvert_add_rule("builtins:NoneType", Nothing, pyconvert_rule_none, priority) pyconvert_add_rule("builtins:bool", Bool, pyconvert_rule_bool, priority) diff --git a/src/Convert/rules.jl b/src/Convert/rules.jl index 829a796a..ba9d689b 100644 --- a/src/Convert/rules.jl +++ b/src/Convert/rules.jl @@ -439,3 +439,16 @@ function pyconvert_rule_timedelta(::Type{Second}, x::Py) end return Second(days * 3600 * 24 + seconds) end + +function pyconvert_rule_timedelta(::Type{<:CompoundPeriod}, x::Py) + days = pyconvert(Int, x.days) + seconds = pyconvert(Int, x.seconds) + microseconds = pyconvert(Int, x.microseconds) + nanoseconds = pyhasattr(x, "nanoseconds") ? pyconvert(Int, x.nanoseconds) : 0 + timedelta = Day(days) + Second(seconds) + Microsecond(microseconds) + Nanosecond(nanoseconds) + return pyconvert_return(timedelta) +end + +function pyconvert_rule_timedelta(::Type{T}, x::Py) where T<:Period + pyconvert_return(convert(T, pyconvert_rule_timedelta(CompoundPeriod, x))) +end diff --git a/src/Core/Core.jl b/src/Core/Core.jl index a6e27c37..15cb0b79 100644 --- a/src/Core/Core.jl +++ b/src/Core/Core.jl @@ -14,6 +14,8 @@ using ..GC: GC using ..Utils: Utils using Base: @propagate_inbounds, @kwdef using Dates: Date, Time, DateTime, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond +using Dates: Day, Second, Millisecond, Microsecond, Minute, Hour, Week +using Dates: Period, CompoundPeriod, canonicalize using MacroTools: MacroTools, @capture using Markdown: Markdown diff --git a/src/Core/Py.jl b/src/Core/Py.jl index 396b39d8..945b6939 100644 --- a/src/Core/Py.jl +++ b/src/Core/Py.jl @@ -149,6 +149,7 @@ Py(x::AbstractRange{<:Union{Int8,Int16,Int32,Int64,Int128,UInt8,UInt16,UInt32,UI Py(x::Date) = pydate(x) Py(x::Time) = pytime(x) Py(x::DateTime) = pydatetime(x) +Py(x::Union{Period, CompoundPeriod}) = pytimedelta(x) Base.string(x::Py) = pyisnull(x) ? "" : pystr(String, x) Base.print(io::IO, x::Py) = print(io, string(x)) diff --git a/src/Core/builtins.jl b/src/Core/builtins.jl index 165e281c..6df4201f 100644 --- a/src/Core/builtins.jl +++ b/src/Core/builtins.jl @@ -1065,6 +1065,24 @@ end pydatetime(x::Date) = pydatetime(year(x), month(x), day(x)) export pydatetime +function pytimedelta( + _day::Int=0, _second::Int=0, _microsecond::Int=0, _millisecond::Int=0, _minute::Int=0, _hour::Int=0, _week::Int=0; + day::Int=_day, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, minute::Int=_minute, hour::Int=_hour, week::Int=_week +) + pyimport("datetime").timedelta(day, second, microsecond, millisecond, minute, hour, week) +end +function pytimedelta(@nospecialize(x::T)) where T <: Period + T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || + error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.") + args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) + pytimedelta(x.value .* args...) +end +function pytimedelta(x::CompoundPeriod) + x = canonicalize(x) + isempty(x.periods) ? pytimedelta(Second(0)) : sum(pytimedelta.(x.periods)) +end +export pytimedelta + function pytime_isaware(x) tzinfo = pygetattr(x, "tzinfo") if pyisnone(tzinfo) From f897600b25fedc31ce2b3c378dca61d68d1e95b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Mon, 17 Jun 2024 00:20:12 +0200 Subject: [PATCH 03/20] fix week kw in pytimedelta64, typo (space) in builtins --- src/Convert/numpy.jl | 2 +- src/Core/builtins.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 2634ac0d..9909fed4 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -51,7 +51,7 @@ function pytimedelta64( year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, nanosecond::Int=_nanosecond, week::Int=_week) pytimedelta64(sum(( Year(year), Month(month), # you cannot mix year or month with any of the below units in python, the error will be thrown by `pytimedelta64(::CompoundPeriod)` - Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond)) + Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond), Week(week)) )) end function pytimedelta64(@nospecialize(x::T)) where T <: Period diff --git a/src/Core/builtins.jl b/src/Core/builtins.jl index 6df4201f..9b78c7e8 100644 --- a/src/Core/builtins.jl +++ b/src/Core/builtins.jl @@ -1074,7 +1074,7 @@ end function pytimedelta(@nospecialize(x::T)) where T <: Period T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.") - args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) + args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) pytimedelta(x.value .* args...) end function pytimedelta(x::CompoundPeriod) From 1e8d4105c2aa14fdef0078e7f911d8a875e54327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Mon, 17 Jun 2024 10:43:12 +0200 Subject: [PATCH 04/20] correct handling of count in 64-bit conversion rules --- src/Convert/numpy.jl | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 9909fed4..bb7bacee 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -66,25 +66,21 @@ end export pytimedelta64 function pyconvert_rule_datetime64(::Type{DateTime}, x::Py) - unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) - # strangely, datetime_data does not return the value correctly - # so we retrieve the value from the byte representation + unit, count = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) value = reinterpret(Int64, pyconvert(Vector, x))[1] units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) T = types[findfirst(==(unit), units)] - pyconvert_return(DateTime(_base_datetime) + T(value)) + pyconvert_return(DateTime(_base_datetime) + T(value * count)) end function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py) - unit, value = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) - # strangely, datetime_data does not return the value correctly - # so we retrieve the value from the byte representation + unit, count = pyconvert(Tuple, pyimport("numpy").datetime_data(x)) value = reinterpret(Int64, pyconvert(Vector, x))[1] units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) T = types[findfirst(==(unit), units)] - pyconvert_return(CompoundPeriod(T(value)) |> canonicalize) + pyconvert_return(CompoundPeriod(T(value * count)) |> canonicalize) end function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period From 9dfc0ddaaabb997f887b07302ebfe67d766708e5 Mon Sep 17 00:00:00 2001 From: hhaensel <31985040+hhaensel@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:42:12 +0200 Subject: [PATCH 05/20] Apply suggestions from code review Co-authored-by: Miles Cranmer --- src/Convert/numpy.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 45b4c8bc..716eb50c 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -36,13 +36,13 @@ function pydatetime64( end function pydatetime64(@nospecialize(x::T)) where T <: Period T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || - error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.") - args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) + error("Unsupported Period type: `$x::$T`. Consider using pytimedelta64 instead.") + args = map(Base.Fix1(isa, x), (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)) pydatetime64(x.value .* args...) end function pydatetime64(x::CompoundPeriod) x = canonicalize(x) - isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64.(x.periods)) + isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64, x.periods) end export pydatetime64 @@ -55,7 +55,7 @@ function pytimedelta64( )) end function pytimedelta64(@nospecialize(x::T)) where T <: Period - index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T)) + index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))::Int unit = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "")[index] pyimport("numpy").timedelta64(x.value, unit) end @@ -70,7 +70,7 @@ function pyconvert_rule_datetime64(::Type{DateTime}, x::Py) value = reinterpret(Int64, pyconvert(Vector, x))[1] units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) - T = types[findfirst(==(unit), units)] + T = types[findfirst(==(unit), units)::Int] pyconvert_return(DateTime(_base_datetime) + T(value * count)) end @@ -79,7 +79,7 @@ function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py) value = reinterpret(Int64, pyconvert(Vector, x))[1] units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) - T = types[findfirst(==(unit), units)] + T = types[findfirst(==(unit), units)::Int] pyconvert_return(CompoundPeriod(T(value * count)) |> canonicalize) end From a3a2b9713ba7d873b89efa19c765726037333daf Mon Sep 17 00:00:00 2001 From: hhaensel <31985040+hhaensel@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:44:41 +0200 Subject: [PATCH 06/20] Apply suggestions from code review part II Co-authored-by: Miles Cranmer --- src/Convert/numpy.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 716eb50c..0c025e37 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -38,7 +38,7 @@ function pydatetime64(@nospecialize(x::T)) where T <: Period T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || error("Unsupported Period type: `$x::$T`. Consider using pytimedelta64 instead.") args = map(Base.Fix1(isa, x), (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)) - pydatetime64(x.value .* args...) + pydatetime64(map(Base.Fix1(*, x.value), args)...) end function pydatetime64(x::CompoundPeriod) x = canonicalize(x) From daf97594acfb0e7a46d86bb1d975938cc23fc59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Sat, 7 Sep 2024 01:34:33 +0200 Subject: [PATCH 07/20] reviewers suggestions part III --- src/Convert/numpy.jl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 0c025e37..6d830573 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -28,9 +28,9 @@ const NUMPY_SIMPLE_TYPES = [ ] function pydatetime64( - _year::Int=0, _month::Int=1, _day::Int=1, _hour::Int=0, _minute::Int=0,_second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0; - year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, - millisecond::Int=_millisecond, microsecond::Int=_microsecond, nanosecond::Int=_nanosecond + _year::Integer=0, _month::Integer=1, _day::Integer=1, _hour::Integer=0, _minute::Integer=0,_second::Integer=0, _millisecond::Integer=0, _microsecond::Integer=0, _nanosecond::Integer=0; + year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second, + millisecond::Integer=_millisecond, microsecond::Integer=_microsecond, nanosecond::Integer=_nanosecond ) pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + pytimedelta64(;millisecond, microsecond, nanosecond) end @@ -47,10 +47,12 @@ end export pydatetime64 function pytimedelta64( - _year::Int=0, _month::Int=0, _day::Int=0, _hour::Int=0, _minute::Int=0, _second::Int=0, _millisecond::Int=0, _microsecond::Int=0, _nanosecond::Int=0, _week::Int=0; - year::Int=_year, month::Int=_month, day::Int=_day, hour::Int=_hour, minute::Int=_minute, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, nanosecond::Int=_nanosecond, week::Int=_week) + _year::Integer=0, _month::Integer=0, _day::Integer=0, _hour::Integer=0, _minute::Integer=0, _second::Integer=0, _millisecond::Integer=0, _microsecond::Integer=0, _nanosecond::Integer=0, _week::Integer=0; + year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second, microsecond::Integer=_microsecond, millisecond::Integer=_millisecond, nanosecond::Integer=_nanosecond, week::Integer=_week) pytimedelta64(sum(( - Year(year), Month(month), # you cannot mix year or month with any of the below units in python, the error will be thrown by `pytimedelta64(::CompoundPeriod)` + Year(year), Month(month), + # you cannot mix year or month with any of the below units in python + # in case of wrong usage a descriptive error message will by thrown by the underlying python function Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond), Week(week)) )) end @@ -117,8 +119,8 @@ function init_numpy() priority = PYCONVERT_PRIORITY_ARRAY pyconvert_add_rule("numpy:datetime64", DateTime, pyconvert_rule_datetime64, priority) - for T in (CompoundPeriod, Year, Month, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, Week) - pyconvert_add_rule("numpy:timedelta64", T, pyconvert_rule_timedelta64, priority) + let TT = (CompoundPeriod, Year, Month, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, Week) + Base.Cartesian.@nexprs 11 i -> pyconvert_add_rule("numpy:timedelta64", TT[i], pyconvert_rule_timedelta64, priority) end priority = PYCONVERT_PRIORITY_CANONICAL From 7391b8d3682b149806368741af0ae29c517f9ca2 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 19 Jan 2025 12:08:57 +0100 Subject: [PATCH 08/20] add tests for pytimedelta --- test/Core.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Core.jl b/test/Core.jl index c5a9b48e..feadf855 100644 --- a/test/Core.jl +++ b/test/Core.jl @@ -698,6 +698,9 @@ end x7 = pydatetime(DateTime(2001, 2, 3, 4, 5, 6, 7)) @test pyisinstance(x7, dt.datetime) @test pyeq(Bool, x7, dt.datetime(2001, 2, 3, 4, 5, 6, 7000)) + x8 = pydatetime(2001, 2, 3, 4, 5, 6, 7) + dx = pytimedelta(366, 3661, 1) + pyeq(Bool, x8 - x6, dx) end @testitem "code" begin From 8f285670d13362b041d4e1cd23fdf96e8171d6df Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 20 Jan 2025 16:23:46 +0100 Subject: [PATCH 09/20] fix micro/millisecond in pytimedelta, append 's' to keywords in pytimedelta and pytimedelta64 --- src/Convert/numpy.jl | 11 ++++++----- src/Core/builtins.jl | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index 6d830573..b43544e7 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -32,7 +32,8 @@ function pydatetime64( year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second, millisecond::Integer=_millisecond, microsecond::Integer=_microsecond, nanosecond::Integer=_nanosecond ) - pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + pytimedelta64(;millisecond, microsecond, nanosecond) + pyimport("numpy").datetime64("$(DateTime(year, month, day, hour, minute, second))") + + pytimedelta64(; milliseconds = millisecond, microseconds = microsecond, nanoseconds = nanosecond) end function pydatetime64(@nospecialize(x::T)) where T <: Period T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || @@ -47,13 +48,13 @@ end export pydatetime64 function pytimedelta64( - _year::Integer=0, _month::Integer=0, _day::Integer=0, _hour::Integer=0, _minute::Integer=0, _second::Integer=0, _millisecond::Integer=0, _microsecond::Integer=0, _nanosecond::Integer=0, _week::Integer=0; - year::Integer=_year, month::Integer=_month, day::Integer=_day, hour::Integer=_hour, minute::Integer=_minute, second::Integer=_second, microsecond::Integer=_microsecond, millisecond::Integer=_millisecond, nanosecond::Integer=_nanosecond, week::Integer=_week) + _years::Integer=0, _months::Integer=0, _days::Integer=0, _hours::Integer=0, _minutes::Integer=0, _seconds::Integer=0, _milliseconds::Integer=0, _microseconds::Integer=0, _nanoseconds::Integer=0, _weeks::Integer=0; + years::Integer=_years, months::Integer=_months, days::Integer=_days, hours::Integer=_hours, minutes::Integer=_minutes, seconds::Integer=_seconds, microseconds::Integer=_microseconds, milliseconds::Integer=_milliseconds, nanoseconds::Integer=_nanoseconds, weeks::Integer=_weeks) pytimedelta64(sum(( - Year(year), Month(month), + Year(years), Month(months), # you cannot mix year or month with any of the below units in python # in case of wrong usage a descriptive error message will by thrown by the underlying python function - Day(day), Hour(hour), Minute(minute), Second(second), Millisecond(millisecond), Microsecond(microsecond), Nanosecond(nanosecond), Week(week)) + Day(days), Hour(hours), Minute(minutes), Second(seconds), Millisecond(milliseconds), Microsecond(microseconds), Nanosecond(nanoseconds), Week(weeks)) )) end function pytimedelta64(@nospecialize(x::T)) where T <: Period diff --git a/src/Core/builtins.jl b/src/Core/builtins.jl index 97c80770..4559428a 100644 --- a/src/Core/builtins.jl +++ b/src/Core/builtins.jl @@ -1168,15 +1168,15 @@ pydatetime(x::Date) = pydatetime(year(x), month(x), day(x)) export pydatetime function pytimedelta( - _day::Int=0, _second::Int=0, _microsecond::Int=0, _millisecond::Int=0, _minute::Int=0, _hour::Int=0, _week::Int=0; - day::Int=_day, second::Int=_second, microsecond::Int=_microsecond, millisecond::Int=_millisecond, minute::Int=_minute, hour::Int=_hour, week::Int=_week + _days::Int=0, _seconds::Int=0, _microseconds::Int=0, _milliseconds::Int=0, _minutes::Int=0, _hours::Int=0, _weeks::Int=0; + days::Int=_days, seconds::Int=_seconds, microseconds::Int=_microseconds, milliseconds::Int=_milliseconds, minutes::Int=_minutes, hours::Int=_hours, weeks::Int=_weeks ) - pyimport("datetime").timedelta(day, second, microsecond, millisecond, minute, hour, week) + pyimport("datetime").timedelta(days, seconds, microseconds, milliseconds, minutes, hours, weeks) end function pytimedelta(@nospecialize(x::T)) where T <: Period T <: Union{Week, Day, Hour, Minute, Second, Millisecond, Microsecond} || error("Unsupported Period type: ", "Year, Month and Nanosecond are not supported, consider using pytimedelta64 instead.") - args = T .== (Day, Second, Millisecond, Microsecond, Minute, Hour, Week) + args = T .== (Day, Second, Microsecond, Millisecond, Minute, Hour, Week) pytimedelta(x.value .* args...) end function pytimedelta(x::CompoundPeriod) From 46efe539cf147345a5207619fcf626ad8a7facd1 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 20 Jan 2025 16:24:02 +0100 Subject: [PATCH 10/20] add tests for pytimedelta, pytimedelta64 and conversion of pytimedelta64 in DataFrames --- test/Convert.jl | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ test/Core.jl | 31 ++++++++++++++++++++++++++++ test/runtests.jl | 2 +- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/test/Convert.jl b/test/Convert.jl index 4f137459..becb06e8 100644 --- a/test/Convert.jl +++ b/test/Convert.jl @@ -305,6 +305,59 @@ end @test_throws Exception pyconvert(Second, td(microseconds = 1000)) end +@testitem "timedelta64" begin + using Dates + using CondaPkg + CondaPkg.add("pandas") + using DataFrames + + dt1 = pytimedelta(seconds = 1) + dt2 = pytimedelta64(seconds = 1) + @test pyeq(Bool, dt1, dt2) + + @test pyeq(Bool, pytimedelta64(seconds = 10), pyimport("numpy").timedelta64(10, "s")) + @test pyeq(Bool, pytimedelta64(years = 10), pyimport("numpy").timedelta64(10, "Y")) + @test_throws Exception pytimedelta64(years = 10, seconds = 1) + + @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, + ], (Unit, unit) in [ + (Nanosecond, :nanoseconds), + (Microsecond, :microseconds), + (Millisecond, :milliseconds), + (Second, :seconds), + (Minute, :minutes), + (Hour, :hours), + (Day, :days), + (Week, :weeks), + (Month, :months), + (Year, :years), + ] + y = pyconvert(Unit, pytimedelta64(; [unit => x]...)) + @test y === Unit(x) + end + @test_throws Exception pyconvert(Second, td(microseconds = 1000)) + + jdf = DataFrame(x = [now() + Second(rand(1:1000)) for _ in 1:100], y = [Second(n) for n in 1:100]) + pdf = pytable(jdf) + @test ispy(pdf.y) + @test pyeq(Bool, pdf.y[0], pytimedelta64(seconds = 1)) + # automatic conversion from pytimedelta64 converts to Dates.CompoundPeriod + jdf2 = DataFrame(PyPandasDataFrame(pdf)) + @test eltype(jdf2.y) == Dates.CompoundPeriod + # convert y column back to Seconds + jdf2.y = convert.(Second, jdf2.y) + @test pyeq(Bool, jdf, jdf2) +end + @testitem "pyconvert_add_rule (#364)" begin id = string(rand(UInt128), base = 16) pyexec( diff --git a/test/Core.jl b/test/Core.jl index feadf855..e8375f00 100644 --- a/test/Core.jl +++ b/test/Core.jl @@ -676,6 +676,9 @@ end @testitem "datetime" begin using Dates + using CondaPkg + CondaPkg.add("numpy") + dt = pyimport("datetime") x1 = pydate(2001, 2, 3) @test pyisinstance(x1, dt.date) @@ -701,6 +704,34 @@ end x8 = pydatetime(2001, 2, 3, 4, 5, 6, 7) dx = pytimedelta(366, 3661, 1) pyeq(Bool, x8 - x6, dx) + + td = pyimport("datetime").timedelta + @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, + ], (Unit, unit, pyunit, factor) in [ + (Microsecond, :microseconds, :microseconds, 1), + (Millisecond, :milliseconds, :milliseconds, 1), + (Second, :seconds, :seconds, 1), + (Minute, :minutes, :seconds, 60), + (Hour, :hours, :hours, 1), + (Day, :days, :days, 1), + (Week, :weeks, :days, 7) + ] + # for day and week units, skip large values due to overflow + unit in [:days, :weeks] && abs(x) > 1_000_000 && continue + y = pytimedelta(; [unit => x]...) + y2 = pytimedelta(Unit(x)) + @test pyeq(Bool, y, y2) + @test pyeq(Bool, y, td(; [pyunit => x * factor]...)) + end end @testitem "code" begin diff --git a/test/runtests.jl b/test/runtests.jl index b9e874db..0660c407 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,3 @@ using TestItemRunner - +using DataFrames @run_package_tests From d36c113d5ea8356593ff8680672affce1ce32836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Tue, 21 Jan 2025 01:13:27 +0100 Subject: [PATCH 11/20] fix pytimdelta(years/months=0), add pydatetime64(::Union{Date, DateTime}), remove pydatetime64(::CompoundPeriod) --- src/Convert/numpy.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index b43544e7..b33a1424 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -41,21 +41,29 @@ function pydatetime64(@nospecialize(x::T)) where T <: Period args = map(Base.Fix1(isa, x), (Day, Second, Millisecond, Microsecond, Minute, Hour, Week)) pydatetime64(map(Base.Fix1(*, x.value), args)...) end -function pydatetime64(x::CompoundPeriod) - x = canonicalize(x) - isempty(x.periods) ? pydatetime64(Second(0)) : sum(pydatetime64, x.periods) +function pydatetime64(x::Union{Date, DateTime}) + pyimport("numpy").datetime64("$x") end export pydatetime64 function pytimedelta64( - _years::Integer=0, _months::Integer=0, _days::Integer=0, _hours::Integer=0, _minutes::Integer=0, _seconds::Integer=0, _milliseconds::Integer=0, _microseconds::Integer=0, _nanoseconds::Integer=0, _weeks::Integer=0; - years::Integer=_years, months::Integer=_months, days::Integer=_days, hours::Integer=_hours, minutes::Integer=_minutes, seconds::Integer=_seconds, microseconds::Integer=_microseconds, milliseconds::Integer=_milliseconds, nanoseconds::Integer=_nanoseconds, weeks::Integer=_weeks) - pytimedelta64(sum(( - Year(years), Month(months), + _years::Union{Nothing,Integer}=nothing, _months::Union{Nothing,Integer}=nothing, _days::Integer=0, _hours::Integer=0, _minutes::Integer=0, _seconds::Integer=0, _milliseconds::Integer=0, _microseconds::Integer=0, _nanoseconds::Integer=0, _weeks::Integer=0; + years::Union{Nothing,Integer}=_years, months::Union{Nothing,Integer}=_years, days::Integer=_days, hours::Integer=_hours, minutes::Integer=_minutes, seconds::Integer=_seconds, microseconds::Integer=_microseconds, milliseconds::Integer=_milliseconds, nanoseconds::Integer=_nanoseconds, weeks::Integer=_weeks) + year_or_month_given = (years !== nothing || months !== nothing) + y::Integer = something(years, 0) + m::Integer = something(months, 0) + cp = sum(( + Year(y), Month(m), # you cannot mix year or month with any of the below units in python # in case of wrong usage a descriptive error message will by thrown by the underlying python function Day(days), Hour(hours), Minute(minutes), Second(seconds), Millisecond(milliseconds), Microsecond(microseconds), Nanosecond(nanoseconds), Week(weeks)) - )) + ) + # make sure the correct unit is used when value is 0 + if isempty(cp.periods) && year_or_month_given + pytimedelta64(Month(0)) + else + pytimedelta64(cp) + end end function pytimedelta64(@nospecialize(x::T)) where T <: Period index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))::Int From 66459c6472f1632273fa4414195fe7794a360163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= Date: Tue, 21 Jan 2025 01:13:47 +0100 Subject: [PATCH 12/20] add tests for pytimedelta64, pydatetime64 --- test/Core.jl | 3 --- test/Numpy.jl | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 test/Numpy.jl diff --git a/test/Core.jl b/test/Core.jl index e8375f00..d9e319d3 100644 --- a/test/Core.jl +++ b/test/Core.jl @@ -676,9 +676,6 @@ end @testitem "datetime" begin using Dates - using CondaPkg - CondaPkg.add("numpy") - dt = pyimport("datetime") x1 = pydate(2001, 2, 3) @test pyisinstance(x1, dt.date) diff --git a/test/Numpy.jl b/test/Numpy.jl new file mode 100644 index 00000000..1d812106 --- /dev/null +++ b/test/Numpy.jl @@ -0,0 +1,57 @@ +@testitem "timedelta64" begin + using Dates + using CondaPkg + CondaPkg.add("numpy") + + td = pyimport("numpy").timedelta64 + @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, + ], (Unit, unit, pyunit) in [ + (Nanosecond, :nanoseconds, :ns), + (Microsecond, :microseconds, :us), + (Millisecond, :milliseconds, :ms), + (Second, :seconds, :s), + (Minute, :minutes, :m), + (Hour, :hours, :h), + (Day, :days, :D), + (Week, :weeks, :W), + (Month, :months, :M), + (Year, :years, :Y), + ] + y = pytimedelta64(; [unit => x]...) + y2 = pytimedelta64(Unit(x)) + @test pyeq(Bool, y, y2) + @test pyeq(Bool, y, td(x, "$pyunit")) + end +end + +@testitem "datetime64" begin + using Dates + using CondaPkg + CondaPkg.add("numpy") + + y = 2024 + m = 2 + d = 29 + h = 23 + min = 59 + s = 58 + ms = 999 + us = 998 + ns = 997 + + date = DateTime(y, m, d, h, min, s, ms) + pydate = pydatetime64(date) + pydate2 = pydatetime64(year = y, month = m, day = d, hour = h, minute = min, second = s, millisecond = ms) + dt = date - Second(0) + pydate3 = pydatetime64(dt) + @test pyeq(Bool, pydate, pydate2) +end \ No newline at end of file From 3c51b54cd69054f1e605de63b28605a0d64d549f Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 09:42:27 +0100 Subject: [PATCH 13/20] support unitless timedelta64, keep unit per default, add keyword canonicalize, add global setting CANONICALIZE_TIMEDELTA64 for rule conversion --- src/Convert/numpy.jl | 55 ++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Convert/numpy.jl b/src/Convert/numpy.jl index b33a1424..1013290f 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -1,3 +1,5 @@ +const CANONICALIZE_TIMEDELTA64 = Ref(false) + struct pyconvert_rule_numpysimplevalue{R,S} <: Function end function (::pyconvert_rule_numpysimplevalue{R,SAFE})(::Type{T}, x::Py) where {R,SAFE,T} @@ -47,33 +49,54 @@ end export pydatetime64 function pytimedelta64( - _years::Union{Nothing,Integer}=nothing, _months::Union{Nothing,Integer}=nothing, _days::Integer=0, _hours::Integer=0, _minutes::Integer=0, _seconds::Integer=0, _milliseconds::Integer=0, _microseconds::Integer=0, _nanoseconds::Integer=0, _weeks::Integer=0; - years::Union{Nothing,Integer}=_years, months::Union{Nothing,Integer}=_years, days::Integer=_days, hours::Integer=_hours, minutes::Integer=_minutes, seconds::Integer=_seconds, microseconds::Integer=_microseconds, milliseconds::Integer=_milliseconds, nanoseconds::Integer=_nanoseconds, weeks::Integer=_weeks) - year_or_month_given = (years !== nothing || months !== nothing) + _years::Union{Nothing,Integer}=nothing, _months::Union{Nothing,Integer}=nothing, + _days::Union{Nothing,Integer}=nothing, _hours::Union{Nothing,Integer}=nothing, + _minutes::Union{Nothing,Integer}=nothing, _seconds::Union{Nothing,Integer}=nothing, + _milliseconds::Union{Nothing,Integer}=nothing, _microseconds::Union{Nothing,Integer}=nothing, + _nanoseconds::Union{Nothing,Integer}=nothing, _weeks::Union{Nothing,Integer}=nothing; + years::Union{Nothing,Integer}=_years, months::Union{Nothing,Integer}=_months, + days::Union{Nothing,Integer}=_days, hours::Union{Nothing,Integer}=_hours, + minutes::Union{Nothing,Integer}=_minutes, seconds::Union{Nothing,Integer}=_seconds, + milliseconds::Union{Nothing,Integer}=_milliseconds, microseconds::Union{Nothing,Integer}=_microseconds, + nanoseconds::Union{Nothing,Integer}=_nanoseconds, weeks::Union{Nothing,Integer}=_weeks, + canonicalize::Bool = false) + y::Integer = something(years, 0) m::Integer = something(months, 0) + d::Integer = something(days, 0) + h::Integer = something(hours, 0) + min::Integer = something(minutes, 0) + s::Integer = something(seconds, 0) + ms::Integer = something(milliseconds, 0) + µs::Integer = something(microseconds, 0) + ns::Integer = something(nanoseconds, 0) + w::Integer = something(weeks, 0) cp = sum(( - Year(y), Month(m), - # you cannot mix year or month with any of the below units in python - # in case of wrong usage a descriptive error message will by thrown by the underlying python function - Day(days), Hour(hours), Minute(minutes), Second(seconds), Millisecond(milliseconds), Microsecond(microseconds), Nanosecond(nanoseconds), Week(weeks)) + Year(y), Month(m), Week(w), Day(d), Hour(h), Minute(min), Second(s), Millisecond(ms), Microsecond(µs), Nanosecond(ns)) ) # make sure the correct unit is used when value is 0 - if isempty(cp.periods) && year_or_month_given - pytimedelta64(Month(0)) + if isempty(cp.periods) + Units = (Second, Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) + index::Integer = findlast(!isnothing, (0, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); + pytimedelta64(Units[index](0)) else - pytimedelta64(cp) + pytimedelta64(cp; canonicalize) end end -function pytimedelta64(@nospecialize(x::T)) where T <: Period - index = findfirst(==(T), (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))::Int +function pytimedelta64(@nospecialize(x::T); canonicalize::Bool = false) where T <: Period + canonicalize && return pytimedelta64(@__MODULE__().canonicalize(x)) + + index = findfirst(T .== (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond, T))::Int unit = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns", "")[index] pyimport("numpy").timedelta64(x.value, unit) end -function pytimedelta64(x::CompoundPeriod) - x = canonicalize(x) +function pytimedelta64(x::CompoundPeriod; canonicalize::Bool = false) + canonicalize && (x = @__MODULE__().canonicalize(x)) isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods)) end +function pytimedelta64(x::Integer) + pyimport("numpy").timedelta64(x) +end export pytimedelta64 function pyconvert_rule_datetime64(::Type{DateTime}, x::Py) @@ -91,7 +114,9 @@ function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py) units = ("Y", "M", "W", "D", "h", "m", "s", "ms", "us", "ns") types = (Year, Month, Week, Day, Hour, Minute, Second, Millisecond, Microsecond, Nanosecond) T = types[findfirst(==(unit), units)::Int] - pyconvert_return(CompoundPeriod(T(value * count)) |> canonicalize) + cp = CompoundPeriod(T(value * count)) + CANONICALIZE_TIMEDELTA64[] && (cp = @__MODULE__().canonicalize(cp)) + pyconvert_return(cp) end function pyconvert_rule_timedelta64(::Type{T}, x::Py) where T<:Period From fac1ef8c97b0672a7d59d93cacd01286b0b6d22d Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 09:43:35 +0100 Subject: [PATCH 14/20] add tests for timedelta64 canonicalize --- test/Numpy.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Numpy.jl b/test/Numpy.jl index 1d812106..7ab9352f 100644 --- a/test/Numpy.jl +++ b/test/Numpy.jl @@ -4,6 +4,7 @@ CondaPkg.add("numpy") td = pyimport("numpy").timedelta64 + get_unit(x) = pyconvert(String, pyimport("numpy").datetime_data(x)[0]) @testset for x in [ -1_000_000_000, -1_000_000, @@ -30,7 +31,18 @@ y2 = pytimedelta64(Unit(x)) @test pyeq(Bool, y, y2) @test pyeq(Bool, y, td(x, "$pyunit")) + @test get_unit(y) == "$pyunit" + @test get_unit(y2) == "$pyunit" end + x = pytimedelta64(Second(60)) + @test get_unit(x) == "s" + x = pytimedelta64(Second(60); canonicalize = true) + @test get_unit(x) == "m" + + PythonCall.Convert.CANONICALIZE_TIMEDELTA64[] = true + @test pyconvert(Dates.CompoundPeriod, pytimedelta64(Second(60)),).periods[1] isa Minute + PythonCall.Convert.CANONICALIZE_TIMEDELTA64[] = false + @test pyconvert(Dates.CompoundPeriod, pytimedelta64(Second(60)),).periods[1] isa Second end @testitem "datetime64" begin From 5abcf1d3dbdc6e7f2d02fff5df9c1cd92ae6c8fe Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 15:44:59 +0100 Subject: [PATCH 15/20] add CondaPkg and DataFrames as extras in Project.toml --- Project.toml | 5 ++++- test/runtests.jl | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 0a86bf7a..25f9956b 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39" [compat] Aqua = "0 - 999" CondaPkg = "0.2.23" +DataFrames = "1.7.0" Dates = "1" Libdl = "1" MacroTools = "0.5" @@ -33,8 +34,10 @@ julia = "1.6.1" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Aqua", "Test", "TestItemRunner"] +test = ["Aqua", "CondaPkg", "DataFrames", "Test", "TestItemRunner"] diff --git a/test/runtests.jl b/test/runtests.jl index 0660c407..b9e874db 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,3 @@ using TestItemRunner -using DataFrames + @run_package_tests From 34f35ce42654045b6b761d63f017dccc8dcb1924 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 20 Jan 2025 11:25:28 +0100 Subject: [PATCH 16/20] fix pandas testing --- test/Compat.jl | 7 +++++-- test/Wrap.jl | 26 ++++++++++++++------------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/test/Compat.jl b/test/Compat.jl index 16b3e65a..0b3fc312 100644 --- a/test/Compat.jl +++ b/test/Compat.jl @@ -79,11 +79,14 @@ end end @testitem "Tables.jl" begin + using CondaPkg + CondaPkg.add("pandas") + @testset "pytable" begin x = (x = [1, 2, 3], y = ["a", "b", "c"]) # pandas - # TODO: install pandas and test properly - @test_throws PyException pytable(x, :pandas) + t = pytable(x, :pandas) + @test pyconvert.(Int, Tuple(t.shape)) == (3, 2) # columns y = pytable(x, :columns) @test pyeq(Bool, y, pydict(x = [1, 2, 3], y = ["a", "b", "c"])) diff --git a/test/Wrap.jl b/test/Wrap.jl index a1e2a359..03991e2c 100644 --- a/test/Wrap.jl +++ b/test/Wrap.jl @@ -439,24 +439,26 @@ end @testitem "PyPandasDataFrame" begin using Tables + using DataFrames + using CondaPkg + CondaPkg.add("pandas") @test PyPandasDataFrame isa Type - # TODO: figure out how to get pandas into the test environment - # for now use some dummy type and take advantage of the fact that the code doesn't actually check it's a real dataframe - @pyexec """ - class DataFrame: - def __init__(self, **kw): - self.__dict__.update(kw) - """ => DataFrame - df = DataFrame(shape = (4, 3), columns = pylist(["foo", "bar", "baz"])) - x = PyPandasDataFrame(df) + x = (x = [1, 2, 3], y = ["a", "b", "c"]) + py_df = pytable(x, :pandas) + @test Tables.istable(PyTable(py_df)) + df = DataFrame(PyTable(py_df)) + @test df == DataFrame(x = [1, 2, 3], y = ["a", "b", "c"]) + + x = PyPandasDataFrame(py_df) + df = DataFrame(x) + @test df == DataFrame(x = [1, 2, 3], y = ["a", "b", "c"]) @test ispy(x) - @test Py(x) === df @test Tables.istable(x) @test Tables.columnaccess(x) - @test_throws Exception Tables.columns(x) + @test Tables.columns(x)[:x] == [1, 2, 3] @test_throws Exception pyconvert(PyPandasDataFrame, 1) str = sprint(show, MIME("text/plain"), x) - @test occursin(r"4×3 .*PyPandasDataFrame", str) + @test occursin(r"3×2 .*PyPandasDataFrame", str) end @testitem "PySet" begin From 986417358bcec78f93bfaf9fdc9d64e8cf7f7a71 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 17:02:13 +0100 Subject: [PATCH 17/20] fix compat with julia < 1.8 --- src/Convert/Convert.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Convert/Convert.jl b/src/Convert/Convert.jl index 99ef7c83..e3145d58 100644 --- a/src/Convert/Convert.jl +++ b/src/Convert/Convert.jl @@ -49,6 +49,12 @@ using Dates: Year, Month, Day, Hour, Minute, Week, Period, CompoundPeriod, canon import ..Core: pyconvert +# patch conversion to Period types for julia <= 1.7 +@static if VERSION < v"1.8.0-" + Base.convert(::Type{T}, x::CompoundPeriod) where T<:Period = + isconcretetype(T) ? sum(T, x.periods; init = zero(T)) : throw(MethodError(convert,(T,x))) +end + include("pyconvert.jl") include("rules.jl") include("ctypes.jl") From 3148bae9cca01a6d580c8ffc70b9d1ec49ea6f3a Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 17:52:20 +0100 Subject: [PATCH 18/20] specialize Base.convert(::<:Period, CompoundPeriod) for julia < 1.8 --- src/Convert/Convert.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Convert/Convert.jl b/src/Convert/Convert.jl index e3145d58..8e97c55d 100644 --- a/src/Convert/Convert.jl +++ b/src/Convert/Convert.jl @@ -51,8 +51,9 @@ import ..Core: pyconvert # patch conversion to Period types for julia <= 1.7 @static if VERSION < v"1.8.0-" - Base.convert(::Type{T}, x::CompoundPeriod) where T<:Period = - isconcretetype(T) ? sum(T, x.periods; init = zero(T)) : throw(MethodError(convert,(T,x))) + for T in (:Year, :Month, :Week, :Day, :Hour, :Minute, :Second, :Millisecond, :Microsecond, :Nanosecond) + @eval Base.convert(::Type{$T}, x::CompoundPeriod) = sum($T, x.periods; init = zero($T)) + end end include("pyconvert.jl") From 94b47df3a8c4d4acd92fce7e4e669df680b57e32 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 18:20:17 +0100 Subject: [PATCH 19/20] change CondaPkg environment --- Project.toml | 14 -------------- test/CondaPkg.toml | 2 ++ test/Project.toml | 6 ++++++ test/Wrap.jl | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) create mode 100644 test/CondaPkg.toml create mode 100644 test/Project.toml diff --git a/Project.toml b/Project.toml index 25f9956b..a1bf45e8 100644 --- a/Project.toml +++ b/Project.toml @@ -16,9 +16,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39" [compat] -Aqua = "0 - 999" CondaPkg = "0.2.23" -DataFrames = "1.7.0" Dates = "1" Libdl = "1" MacroTools = "0.5" @@ -27,17 +25,5 @@ Pkg = "1" Requires = "1" Serialization = "1" Tables = "1" -Test = "1" -TestItemRunner = "0 - 999" UnsafePointers = "1" julia = "1.6.1" - -[extras] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" - -[targets] -test = ["Aqua", "CondaPkg", "DataFrames", "Test", "TestItemRunner"] diff --git a/test/CondaPkg.toml b/test/CondaPkg.toml new file mode 100644 index 00000000..8248a602 --- /dev/null +++ b/test/CondaPkg.toml @@ -0,0 +1,2 @@ +[deps] +pandas = "" diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..0a1205ee --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,6 @@ +[deps] +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" diff --git a/test/Wrap.jl b/test/Wrap.jl index 03991e2c..03deb967 100644 --- a/test/Wrap.jl +++ b/test/Wrap.jl @@ -441,7 +441,7 @@ end using Tables using DataFrames using CondaPkg - CondaPkg.add("pandas") + # CondaPkg.add("pandas") @test PyPandasDataFrame isa Type x = (x = [1, 2, 3], y = ["a", "b", "c"]) py_df = pytable(x, :pandas) From f66f5264c79ff43d9869fd7eefc7bf3b5cbb14c7 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 21 Jan 2025 19:55:52 +0100 Subject: [PATCH 20/20] fix test/Project.toml, adapt runtests.jl --- CondaPkg.toml | 9 --------- test/Project.toml | 14 +++++++++++++- test/runtests.jl | 5 +++-- 3 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 CondaPkg.toml diff --git a/CondaPkg.toml b/CondaPkg.toml deleted file mode 100644 index 7198e26e..00000000 --- a/CondaPkg.toml +++ /dev/null @@ -1,9 +0,0 @@ -[deps.libstdcxx-ng] -version = "<=julia" - -[deps.openssl] -version = "<=julia" - -[deps.python] -build = "**cpython**" -version = ">=3.8,<4" diff --git a/test/Project.toml b/test/Project.toml index 0a1205ee..83f8d875 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,18 @@ [deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" + +[compat] +Aqua = "0 - 999" +CondaPkg = "0.2.23" +DataFrames = "1.7.0" +Dates = "1" +Test = "1" +TestItemRunner = "0 - 999" diff --git a/test/runtests.jl b/test/runtests.jl index b9e874db..986d591c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,4 @@ using TestItemRunner - -@run_package_tests +using CondaPkg +CondaPkg.add("pandas") +@run_package_tests verbose=true