Skip to content

Commit 1232ce6

Browse files
author
Christopher Doris
committed
Merge remote-tracking branch 'origin/main' into pr/JamesWrigley/667
2 parents a6a6726 + ad228ce commit 1232ce6

26 files changed

+2338
-29
lines changed

docs/src/conversion-to-julia.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ From Python, the arguments to a Julia function will be converted according to th
3131
| `datetime.date`/`datetime.time`/`datetime.datetime` | `Date`/`Time`/`DateTime` |
3232
| `datetime.timedelta` | `Microsecond` (or `Millisecond` or `Second` on overflow) |
3333
| `numpy.intXX`/`numpy.uintXX`/`numpy.floatXX` | `IntXX`/`UIntXX`/`FloatXX` |
34+
| `numpy.datetime64` | `NumpyDates.DateTime64` |
35+
| `numpy.timedelta64` | `NumpyDates.TimeDelta64` |
3436
| **Standard priority (other reasonable conversions).** | |
3537
| `None` | `Missing` |
3638
| `bytes` | `Vector{UInt8}`, `Vector{Int8}`, `String` |
@@ -48,6 +50,8 @@ From Python, the arguments to a Julia function will be converted according to th
4850
| `ctypes.c_char_p` | `Cstring`, `Ptr{Cchar}`, `Ptr` |
4951
| `ctypes.c_wchar_p` | `Cwstring`, `Ptr{Cwchar}`, `Ptr` |
5052
| `numpy.bool_`/`numpy.intXX`/`numpy.uintXX`/`numpy.floatXX` | `Bool`, `Integer`, `Rational`, `Real`, `Number` |
53+
| `numpy.datetime64` | `NumpyDates.InlineDateTime64`, `Dates.DateTime` |
54+
| `numpy.timedelta64` | `NumpyDates.InlineTimeDelta64`, `Dates.Period` |
5155
| Objects satisfying the buffer or array interface | `Array`, `AbstractArray` |
5256
| **Low priority (fallback to `Py`).** | |
5357
| Anything | `Py` |

docs/src/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FAQ & Troubleshooting
22

3-
## Can I use PythonCall and PyCall together?
3+
## [Can I use PythonCall and PyCall together?](@id faq-pycall)
44

55
Yes, you can use both PyCall and PythonCall in the same Julia session. This is platform-dependent:
66
- On most systems the Python interpreter used by PythonCall and PyCall must be the same (see below).

docs/src/pythoncall-reference.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,18 @@ PythonCall.getptr
255255
PythonCall.pydel!
256256
PythonCall.unsafe_pynext
257257
```
258+
259+
## NumpyDates
260+
261+
The submodule `PythonCall.NumpyDates` provides types corresponding to Numpy's `datetime64` and `timedelta64` types. Enables conversion of these Numpy types (either as scalars or in arrays) to native Julia types.
262+
263+
```@docs
264+
PythonCall.NumpyDates
265+
PythonCall.NumpyDates.AbstractDateTime64
266+
PythonCall.NumpyDates.InlineDateTime64
267+
PythonCall.NumpyDates.DateTime64
268+
PythonCall.NumpyDates.AbstractTimeDelta64
269+
PythonCall.NumpyDates.InlineTimeDelta64
270+
PythonCall.NumpyDates.TimeDelta64
271+
PythonCall.NumpyDates.Unit
272+
```

docs/src/pythoncall.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,17 +240,21 @@ to your current Julia project containing Python and any required Python packages
240240
ENV["JULIA_CONDAPKG_BACKEND"] = "Null"
241241
ENV["JULIA_PYTHONCALL_EXE"] = "/path/to/python" # optional
242242
ENV["JULIA_PYTHONCALL_EXE"] = "@PyCall" # optional
243+
ENV["JULIA_PYTHONCALL_EXE"] = "@venv" # optional
243244
```
244245

245246
By setting the CondaPkg backend to Null, it will never install any Conda packages. In this
246247
case, PythonCall will use whichever Python is currently installed and in your `PATH`. You
247248
must have already installed any Python packages that you need.
248249

249250
If `python` is not in your `PATH`, you will also need to set `JULIA_PYTHONCALL_EXE` to its
250-
path.
251+
path. Relative paths are resolved relative to the current active project.
251252

252253
If you also use PyCall, you can set `JULIA_PYTHONCALL_EXE=@PyCall` to use the same Python
253-
interpreter.
254+
interpreter. [See here](@ref faq-pycall).
255+
256+
If you have a Python virtual environment at `.venv` in your current active project, you
257+
can set `JULIA_PYTHONCALL_EXE=@venv` to use it.
254258

255259
#### If you already have a Conda environment
256260

docs/src/releasenotes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Release Notes
22

33
## Unreleased
4+
* Added `NumpyDates`: NumPy-compatible DateTime64/TimeDelta64 types and units.
5+
* Added `pyconvert` rules for NumpyDates types.
6+
* Added `PyArray` support for NumPy arrays of `datetime64` and `timedelta64`.
7+
* Added `juliacall.ArrayValue` support for Julia arrays of `InlineDateTime64` and `InlineTimeDelta64`.
8+
* If `JULIA_PYTHONCALL_EXE` is a relative path, it is now considered relative to the active project.
9+
* Added option `JULIA_PYTHONCALL_EXE=@venv` to use a Python virtual environment relative to the active project.
410
* Added `PYTHON_JULIACALL_EXE` and `PYTHON_JULIACALL_PROJECT` for specifying the Julia binary and project to override JuliaPkg.
511
* Bug fixes.
612
* Internal: switch from Requires.jl to package extensions.

src/C/context.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,38 @@ function init_context()
7575
exe_path = PyCall.python::String
7676
CTX.lib_path = PyCall.libpython::String
7777
CTX.which = :PyCall
78+
elseif exe_path == "@venv"
79+
# load from a .venv in the active project
80+
exe_path = abspath(dirname(Base.active_project()), ".venv")
81+
if Sys.iswindows()
82+
exe_path = abspath(exe_path, "Scripts", "python.exe")::String
83+
else
84+
exe_path = abspath(exe_path, "bin", "python")::String
85+
end
7886
elseif startswith(exe_path, "@")
7987
error("invalid JULIA_PYTHONCALL_EXE=$exe_path")
8088
else
8189
# Otherwise we use the Python specified
8290
CTX.which = :unknown
91+
if isabspath(exe_path)
92+
# nothing to do
93+
elseif '/' in exe_path || '\\' in exe_path
94+
# it's a relative path, interpret it as relative to the current project
95+
exe_path = abspath(dirname(Base.active_project()), exe_path)::String
96+
else
97+
# it's a command, find it in the PATH
98+
given_exe_path = exe_path
99+
exe_path = Sys.which(exe_path)
100+
exe_path === nothing &&
101+
error("Python executable $(repr(given_exe_path)) not found.")
102+
exe_path::String
103+
end
83104
end
84105

85106
# Ensure Python is runnable
107+
if !ispath(exe_path)
108+
error("Python executable $(repr(exe_path)) does not exist.")
109+
end
86110
try
87111
run(pipeline(`$exe_path --version`, stdout = devnull, stderr = devnull))
88112
catch

src/Convert/Convert.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ using ..PythonCall
99
using ..Utils
1010
using ..C
1111
using ..Core
12+
using ..NumpyDates
1213

1314
using Dates: Date, Time, DateTime, Second, Millisecond, Microsecond, Nanosecond
1415

@@ -20,8 +21,7 @@ import ..PythonCall:
2021
pyconvert,
2122
PyConvertPriority
2223

23-
export
24-
pyconvert_isunconverted,
24+
export pyconvert_isunconverted,
2525
pyconvert_result,
2626
pyconvert_result,
2727
pyconvert_tryconvert,

src/Convert/numpy.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,76 @@ function (::pyconvert_rule_numpysimplevalue{R,SAFE})(::Type{T}, x::Py) where {R,
99
end
1010
end
1111

12+
function pyconvert_rule_datetime64(::Type{DateTime64}, x::Py)
13+
pyconvert_return(C.PySimpleObject_GetValue(DateTime64, x))
14+
end
15+
16+
function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:InlineDateTime64}
17+
pyconvert_tryconvert(T, C.PySimpleObject_GetValue(DateTime64, x))
18+
end
19+
20+
function pyconvert_rule_datetime64(::Type{T}, x::Py) where {T<:NumpyDates.DatesInstant}
21+
d = C.PySimpleObject_GetValue(DateTime64, x)
22+
if isnan(d)
23+
pyconvert_unconverted()
24+
else
25+
pyconvert_tryconvert(T, d)
26+
end
27+
end
28+
29+
function pyconvert_rule_datetime64(::Type{Missing}, x::Py)
30+
d = C.PySimpleObject_GetValue(DateTime64, x)
31+
if isnan(d)
32+
pyconvert_return(missing)
33+
else
34+
pyconvert_unconverted()
35+
end
36+
end
37+
38+
function pyconvert_rule_datetime64(::Type{Nothing}, x::Py)
39+
d = C.PySimpleObject_GetValue(DateTime64, x)
40+
if isnan(d)
41+
pyconvert_return(nothing)
42+
else
43+
pyconvert_unconverted()
44+
end
45+
end
46+
47+
function pyconvert_rule_timedelta64(::Type{TimeDelta64}, x::Py)
48+
pyconvert_return(C.PySimpleObject_GetValue(TimeDelta64, x))
49+
end
50+
51+
function pyconvert_rule_timedelta64(::Type{T}, x::Py) where {T<:InlineTimeDelta64}
52+
pyconvert_tryconvert(T, C.PySimpleObject_GetValue(TimeDelta64, x))
53+
end
54+
55+
function pyconvert_rule_timedelta64(::Type{T}, x::Py) where {T<:NumpyDates.DatesPeriod}
56+
d = C.PySimpleObject_GetValue(TimeDelta64, x)
57+
if isnan(d)
58+
pyconvert_unconverted()
59+
else
60+
pyconvert_tryconvert(T, d)
61+
end
62+
end
63+
64+
function pyconvert_rule_timedelta64(::Type{Missing}, x::Py)
65+
d = C.PySimpleObject_GetValue(TimeDelta64, x)
66+
if isnan(d)
67+
pyconvert_return(missing)
68+
else
69+
pyconvert_unconverted()
70+
end
71+
end
72+
73+
function pyconvert_rule_timedelta64(::Type{Nothing}, x::Py)
74+
d = C.PySimpleObject_GetValue(TimeDelta64, x)
75+
if isnan(d)
76+
pyconvert_return(missing)
77+
else
78+
pyconvert_unconverted()
79+
end
80+
end
81+
1282
const NUMPY_SIMPLE_TYPES = [
1383
("bool_", Bool),
1484
("int8", Int8),
@@ -28,6 +98,7 @@ const NUMPY_SIMPLE_TYPES = [
2898
]
2999

30100
function init_numpy()
101+
# simple numeric scalar types
31102
for (t, T) in NUMPY_SIMPLE_TYPES
32103
isbool = occursin("bool", t)
33104
isint = occursin("int", t) || isbool
@@ -54,4 +125,36 @@ function init_numpy()
54125
iscomplex && pyconvert_add_rule(name, Complex, rule)
55126
isnumber && pyconvert_add_rule(name, Number, rule)
56127
end
128+
129+
# datetime64
130+
pyconvert_add_rule(
131+
"numpy:datetime64",
132+
DateTime64,
133+
pyconvert_rule_datetime64,
134+
PYCONVERT_PRIORITY_ARRAY,
135+
)
136+
pyconvert_add_rule("numpy:datetime64", InlineDateTime64, pyconvert_rule_datetime64)
137+
pyconvert_add_rule(
138+
"numpy:datetime64",
139+
NumpyDates.DatesInstant,
140+
pyconvert_rule_datetime64,
141+
)
142+
pyconvert_add_rule("numpy:datetime64", Missing, pyconvert_rule_datetime64)
143+
pyconvert_add_rule("numpy:datetime64", Nothing, pyconvert_rule_datetime64)
144+
145+
# timedelta64
146+
pyconvert_add_rule(
147+
"numpy:timedelta64",
148+
TimeDelta64,
149+
pyconvert_rule_timedelta64,
150+
PYCONVERT_PRIORITY_ARRAY,
151+
)
152+
pyconvert_add_rule("numpy:timedelta64", InlineTimeDelta64, pyconvert_rule_timedelta64)
153+
pyconvert_add_rule(
154+
"numpy:timedelta64",
155+
NumpyDates.DatesPeriod,
156+
pyconvert_rule_timedelta64,
157+
)
158+
pyconvert_add_rule("numpy:timedelta64", Missing, pyconvert_rule_timedelta64)
159+
pyconvert_add_rule("numpy:timedelta64", Nothing, pyconvert_rule_timedelta64)
57160
end

src/JlWrap/JlWrap.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module JlWrap
77

88
using ..PythonCall
99
using ..Utils
10+
using ..NumpyDates: NumpyDates
1011
using ..C
1112
using ..Core
1213
using ..Convert

src/JlWrap/array.jl

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ pyjlarray_isarrayabletype(::Type{T}) where {T} = T in (
223223
Complex{Float32},
224224
Complex{Float64},
225225
)
226+
pyjlarray_isarrayabletype(::Type{NumpyDates.InlineDateTime64{U}}) where {U} = true
227+
pyjlarray_isarrayabletype(::Type{NumpyDates.InlineTimeDelta64{U}}) where {U} = true
226228
pyjlarray_isarrayabletype(::Type{T}) where {T<:Tuple} =
227229
isconcretetype(T) &&
228230
Base.allocatedinline(T) &&
@@ -235,22 +237,45 @@ const PYTYPESTRDESCR = IdDict{Type,Tuple{String,Py}}()
235237
pytypestrdescr(::Type{T}) where {T} =
236238
get!(PYTYPESTRDESCR, T) do
237239
c = Utils.islittleendian() ? '<' : '>'
238-
T == Bool ? ("$(c)b$(sizeof(Bool))", PyNULL) :
239-
T == Int8 ? ("$(c)i1", PyNULL) :
240-
T == UInt8 ? ("$(c)u1", PyNULL) :
241-
T == Int16 ? ("$(c)i2", PyNULL) :
242-
T == UInt16 ? ("$(c)u2", PyNULL) :
243-
T == Int32 ? ("$(c)i4", PyNULL) :
244-
T == UInt32 ? ("$(c)u4", PyNULL) :
245-
T == Int64 ? ("$(c)i8", PyNULL) :
246-
T == UInt64 ? ("$(c)u8", PyNULL) :
247-
T == Float16 ? ("$(c)f2", PyNULL) :
248-
T == Float32 ? ("$(c)f4", PyNULL) :
249-
T == Float64 ? ("$(c)f8", PyNULL) :
250-
T == Complex{Float16} ? ("$(c)c4", PyNULL) :
251-
T == Complex{Float32} ? ("$(c)c8", PyNULL) :
252-
T == Complex{Float64} ? ("$(c)c16", PyNULL) :
253-
if isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
240+
if T == Bool
241+
("$(c)b$(sizeof(Bool))", PyNULL)
242+
elseif T == Int8
243+
("$(c)i1", PyNULL)
244+
elseif T == UInt8
245+
("$(c)u1", PyNULL)
246+
elseif T == Int16
247+
("$(c)i2", PyNULL)
248+
elseif T == UInt16
249+
("$(c)u2", PyNULL)
250+
elseif T == Int32
251+
("$(c)i4", PyNULL)
252+
elseif T == UInt32
253+
("$(c)u4", PyNULL)
254+
elseif T == Int64
255+
("$(c)i8", PyNULL)
256+
elseif T == UInt64
257+
("$(c)u8", PyNULL)
258+
elseif T == Float16
259+
("$(c)f2", PyNULL)
260+
elseif T == Float32
261+
("$(c)f4", PyNULL)
262+
elseif T == Float64
263+
("$(c)f8", PyNULL)
264+
elseif T == Complex{Float16}
265+
("$(c)c4", PyNULL)
266+
elseif T == Complex{Float32}
267+
("$(c)c8", PyNULL)
268+
elseif T == Complex{Float64}
269+
("$(c)c16", PyNULL)
270+
elseif isconcretetype(T) &&
271+
T <: Union{NumpyDates.InlineDateTime64,NumpyDates.InlineTimeDelta64}
272+
u, m = NumpyDates.unitpair(T)
273+
tc = T <: NumpyDates.InlineDateTime64 ? 'M' : 'm'
274+
us =
275+
u == NumpyDates.UNBOUND_UNITS ? "" :
276+
m == 1 ? "[$(Symbol(u))]" : "[$(m)$(Symbol(u))]"
277+
("$(c)$(tc)8$(us)", PyNULL)
278+
elseif isstructtype(T) && isconcretetype(T) && Base.allocatedinline(T)
254279
n = fieldcount(T)
255280
flds = []
256281
for i = 1:n

0 commit comments

Comments
 (0)