Repository: JuliaPy/PyCall.jl
Pull request #1069 (2023-12-15 20:16:49)

Add parsing for datetime.time
Using same patterns as elsewhere in this file (and avoiding timezones like in DateTime), add support for parsing times from the Python datetime package to Julia's Dates.Time struct. This came up as a need I had in a personal project and was a quick addition.


Patch:
diff --git a/Project.toml b/Project.toml
index b3cef1f..dd7269e 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "PyCall"
 uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
 authors = ["Steven G. Johnson <stevenj@mit.edu>", "Yichao Yu <yyc1992@gmail.com>", "Takafumi Arakaki <aka.tkf@gmail.com>", "Simon Kornblith <simon@simonster.com>", "Páll Haraldsson <Pall.Haraldsson@gmail.com>", "Jon Malmaud <malmaud@gmail.com>", "Jake Bolewski <jakebolewski@gmail.com>", "Keno Fischer <keno@alumni.harvard.edu>", "Joel Mason <jobba1@hotmail.com>", "Jameson Nash <vtjnash@gmail.com>", "The JuliaPy development team"]
-version = "1.96.3"
+version = "1.96.4"
 
 [deps]
 Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d"
diff --git a/README.md b/README.md
index 9300647..6f9af79 100644
--- a/README.md
+++ b/README.md
@@ -633,7 +633,7 @@ $ source PATH/TO/bin/activate  # activate virtual environment in system shell
 $ julia  # start Julia
 ...
 
-julia> ENV["PYCALL_JL_RUNTIME_PYTHON"] = Sys.which("python")
+julia> ENV["PYCALL_JL_RUNTIME_PYTHON"] = Sys.which("python3")
 "PATH/TO/bin/python3"
 
 julia> using PyCall
diff --git a/deps/buildutils.jl b/deps/buildutils.jl
index 5eb27b9..f090875 100644
--- a/deps/buildutils.jl
+++ b/deps/buildutils.jl
@@ -5,7 +5,15 @@ import Conda, Libdl
 
 pyvar(python::AbstractString, mod::AbstractString, var::AbstractString) = chomp(read(pythonenv(`$python -c "import $mod; print($mod.$(var))"`), String))
 
-pyconfigvar(python::AbstractString, var::AbstractString) = pyvar(python, "distutils.sysconfig", "get_config_var('$(var)')")
+function pyconfigvar(python::AbstractString, var::AbstractString)
+    try
+       pyvar(python, "sysconfig", "get_config_var('$(var)')")
+    catch e
+        emsg = sprint(showerror, e)
+        @warn "Encountered error on using `sysconfig`: $emsg. Falling back to `distutils.sysconfig`."
+        pyvar(python, "distutils.sysconfig", "get_config_var('$(var)')")
+    end
+end
 pyconfigvar(python, var, default) = let v = pyconfigvar(python, var)
     v == "None" ? default : v
 end
diff --git a/src/PyCall.jl b/src/PyCall.jl
index f1987a2..a5d5738 100644
--- a/src/PyCall.jl
+++ b/src/PyCall.jl
@@ -271,22 +271,22 @@ end
 # computing hashes of PyObjects
 
 const pysalt = hash("PyCall.PyObject") # "salt" to mix in to PyObject hashes
-hashsalt(x) = hash(x, pysalt)
 
-function hash(o::PyObject)
+function hash(o::PyObject, salt::UInt)
+    salt ⊻= pysalt
     if ispynull(o)
-        hashsalt(C_NULL)
+        hash(C_NULL, salt)
     elseif is_pyjlwrap(o)
         # call native Julia hash directly on wrapped Julia objects,
         # since on 64-bit Windows the Python 2.x hash is only 32 bits
-        hashsalt(unsafe_pyjlwrap_to_objref(o))
+        hash(unsafe_pyjlwrap_to_objref(o), salt)
     else
         h = ccall((@pysym :PyObject_Hash), Py_hash_t, (PyPtr,), o)
         if h == -1 # error
             pyerr_clear()
-            return hashsalt(PyPtr(o))
+            return hash(PyPtr(o), salt)
         end
-        hashsalt(h)
+        hash(h, salt)
     end
 end
 
diff --git a/src/precompile.jl b/src/precompile.jl
index cdd7590..3cf0aac 100644
--- a/src/precompile.jl
+++ b/src/precompile.jl
@@ -372,7 +372,6 @@ precompile(Tuple{typeof(getproperty), PyDict{Int64, String, true}, Symbol})
 precompile(Tuple{typeof(getproperty), PyError, Symbol})
 precompile(Tuple{typeof(getproperty), PyObject, String})
 precompile(Tuple{typeof(hash), PyObject})
-precompile(Tuple{typeof(hashsalt), Function})
 precompile(Tuple{typeof(hasproperty), PyObject, String})
 precompile(Tuple{typeof(hasproperty), PyObject, Symbol})
 precompile(Tuple{typeof(hassym), Ptr{Nothing}, Symbol})
@@ -528,4 +527,4 @@ precompile(Tuple{typeof(unsafe_load), Ptr{Ptr{PyObject_struct}}})
 precompile(Tuple{typeof(weakref_callback), Ptr{PyObject_struct}, Ptr{PyObject_struct}})
 precompile(Tuple{typeof(xor), Int64, PyObject})
 precompile(Tuple{typeof(xor), PyObject, Int64})
-precompile(Tuple{typeof(xor), PyObject, PyObject})
\ No newline at end of file
+precompile(Tuple{typeof(xor), PyObject, PyObject})
diff --git a/src/pydates.jl b/src/pydates.jl
index 1494095..1b81102 100644
--- a/src/pydates.jl
+++ b/src/pydates.jl
@@ -60,9 +60,11 @@ const PyDate_HEAD = sizeof(Int)+sizeof(PyPtr)+sizeof(Py_hash_t)+1
 const DateType = Ref{PyPtr}(0)
 const DateTimeType = Ref{PyPtr}(0)
 const DeltaType = Ref{PyPtr}(0)
+const TimeType = Ref{PyPtr}(0)
 const Date_FromDate = Ref{Ptr{Cvoid}}(0)
 const DateTime_FromDateAndTime = Ref{Ptr{Cvoid}}(0)
 const Delta_FromDelta = Ref{Ptr{Cvoid}}(0)
+const Time_FromTime = Ref{Ptr{Cvoid}}(0)
 function init_datetime()
     # emulate PyDateTime_IMPORT:
     PyDateTimeAPI = unsafe_load(@pycheckn ccall((@pysym :PyCapsule_Import),
@@ -71,9 +73,11 @@ function init_datetime()
     DateType[] = PyDateTimeAPI.DateType
     DateTimeType[] = PyDateTimeAPI.DateTimeType
     DeltaType[] = PyDateTimeAPI.DeltaType
+    TimeType[] = PyDateTimeAPI.TimeType
     Date_FromDate[] = PyDateTimeAPI.Date_FromDate
     DateTime_FromDateAndTime[] = PyDateTimeAPI.DateTime_FromDateAndTime
     Delta_FromDelta[] = PyDateTimeAPI.Delta_FromDelta
+    Time_FromTime[] = PyDateTimeAPI.Time_FromTime
 end
 
 PyObject(d::Dates.Date) =
@@ -99,6 +103,13 @@ PyDelta_FromDSU(days, seconds, useconds) =
 
 PyObject(p::Dates.Day) = PyDelta_FromDSU(Dates.value(p), 0, 0)
 
+PyObject(t::Dates.Time) =
+    PyObject(@pycheckn ccall(Time_FromTime[], PyPtr,
+                             (Cint, Cint, Cint, Cint, PyPtr, PyPtr),
+                             Dates.hour(t), Dates.minute(t), Dates.second(t),
+                             Dates.millisecond(t) * 1000,
+                             pynothing[], TimeType[]))
+
 function PyObject(p::Dates.Second)
     # normalize to make Cint overflow less likely
     s = Dates.value(p)
@@ -120,12 +131,15 @@ end
 PyDate_Check(o::PyObject) = pyisinstance(o, DateType[])
 PyDateTime_Check(o::PyObject) = pyisinstance(o, DateTimeType[])
 PyDelta_Check(o::PyObject) = pyisinstance(o, DeltaType[])
+PyTime_Check(o::PyObject) = pyisinstance(o, TimeType[])
 
 function pydate_query(o::PyObject)
     if PyDate_Check(o)
         return PyDateTime_Check(o) ? Dates.DateTime : Dates.Date
     elseif PyDelta_Check(o)
         return Dates.Millisecond
+    elseif PyTime_Check(o)
+        return Dates.Time
     else
         return Union{}
     end
@@ -163,6 +177,20 @@ function convert(::Type{Dates.Date}, o::PyObject)
     end
 end
 
+function convert(::Type{Dates.Time}, o::PyObject)
+    if PyTime_Check(o)
+        GC.@preserve o let dt = convert(Ptr{UInt8}, PyPtr(o)) + PyDate_HEAD
+            Dates.Time(unsafe_load(dt,1), unsafe_load(dt,2), # hour, minute
+                    unsafe_load(dt,3), # second
+                    div((UInt(unsafe_load(dt,4)) << 16) |
+                        (UInt(unsafe_load(dt,5)) << 8) |
+                        unsafe_load(dt,6), 1000)) # μs ÷ 1000
+        end
+    else
+        throw(ArgumentError("unknown Time type $o"))
+    end
+end
+
 function delta_dsμ(o::PyObject)
     PyDelta_Check(o) || throw(ArgumentError("$o is not a timedelta instance"))
     p = GC.@preserve o unsafe_load(convert(Ptr{PyDateTime_Delta{Py_hash_t}}, PyPtr(o)))


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 1486c48..5e4e31e 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -167,7 +167,11 @@ const PyInt = pyversion < v"3" ? Int : Clonglong
     end
 
     @test roundtripeq(Dates.Date(2012,3,4))
+    @test_throws ArgumentError convert(Dates.Date, PyObject(42))
     @test roundtripeq(Dates.DateTime(2012,3,4, 7,8,9,11))
+    @test_throws ArgumentError convert(Dates.DateTime, PyObject(42))
+    @test roundtripeq(Dates.Time(7,8,9,11))
+    @test_throws ArgumentError convert(Dates.Time, PyObject(42))
     @test roundtripeq(Dates.Millisecond(typemax(Int32)))
     @test roundtripeq(Dates.Millisecond(typemin(Int32)))
     @test roundtripeq(Dates.Second, Dates.Second(typemax(Int32)))