diff --git a/src/Convert/Convert.jl b/src/Convert/Convert.jl index a97f1a48..99ef7c83 100644 --- a/src/Convert/Convert.jl +++ b/src/Convert/Convert.jl @@ -45,6 +45,7 @@ using ..Core: 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 adb621cc..6d830573 100644 --- a/src/Convert/numpy.jl +++ b/src/Convert/numpy.jl @@ -27,6 +27,68 @@ const NUMPY_SIMPLE_TYPES = [ ("complex128", ComplexF64), ] +function pydatetime64( + _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 +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(map(Base.Fix1(*, 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::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 + # 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 +function pytimedelta64(@nospecialize(x::T)) where T <: Period + 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) + isempty(x.periods) ? pytimedelta64(Second(0)) : sum(pytimedelta64.(x.periods)) +end +export pytimedelta64 + +function pyconvert_rule_datetime64(::Type{DateTime}, x::Py) + 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)::Int] + pyconvert_return(DateTime(_base_datetime) + T(value * count)) +end + +function pyconvert_rule_timedelta64(::Type{CompoundPeriod}, x::Py) + 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)::Int] + pyconvert_return(CompoundPeriod(T(value * count)) |> 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) @@ -54,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) + 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 + 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 c4061b1d..2ef99a07 100644 --- a/src/Convert/pyconvert.jl +++ b/src/Convert/pyconvert.jl @@ -428,6 +428,12 @@ function init_pyconvert() 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 d1028685..81771ed5 100644 --- a/src/Convert/rules.jl +++ b/src/Convert/rules.jl @@ -512,3 +512,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 30d185a7..a8d2ce65 100644 --- a/src/Core/Core.jl +++ b/src/Core/Core.jl @@ -25,7 +25,17 @@ using Dates: second, millisecond, microsecond, - nanosecond + nanosecond, + Day, + Hour, + Week, + Minute, + Second, + Millisecond, + Microsecond, + Period, + CompoundPeriod, + canonicalize using MacroTools: MacroTools, @capture using Markdown: Markdown diff --git a/src/Core/Py.jl b/src/Core/Py.jl index e56d74fa..fb6c1d3b 100644 --- a/src/Core/Py.jl +++ b/src/Core/Py.jl @@ -158,6 +158,7 @@ Py( 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 6a501526..97c80770 100644 --- a/src/Core/builtins.jl +++ b/src/Core/builtins.jl @@ -1167,6 +1167,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) diff --git a/src/PyMacro/PyMacro.jl b/src/PyMacro/PyMacro.jl index 0e978eb6..c48ed8c5 100644 --- a/src/PyMacro/PyMacro.jl +++ b/src/PyMacro/PyMacro.jl @@ -886,6 +886,9 @@ For example: - `import x: f as g` is translated to `g = pyimport("x" => "f")` (`from x import f as g` in Python) 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. @@ -895,6 +898,19 @@ 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 end