Skip to content

Commit 0d4a5ad

Browse files
committed
datetime PyRef idiom
1 parent e93a13d commit 0d4a5ad

20 files changed

Lines changed: 796 additions & 495 deletions

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ bytes = { version = "1", default-features = false }
4848
bytecount = { version = "^0.6.7", default-features = false, features = ["runtime-dispatch-simd"] }
4949
encoding_rs = { version = "0.8", default-features = false }
5050
half = { version = "2", default-features = false }
51-
itoa = { version = "1", default-features = false }
5251
itoap = { version = "1", default-features = false, features = ["std", "simd"] }
5352
jiff = { version = "^0.2", default-features = false }
5453
once_cell = { version = "1", default-features = false, features = ["alloc", "race"] }

src/ffi/compat.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,20 @@ pub(crate) unsafe fn PyTuple_SET_ITEM(
203203
}
204204
}
205205

206+
#[cfg(CPython)]
207+
#[inline(always)]
208+
#[allow(non_snake_case)]
209+
pub(crate) unsafe fn PyObject_Type(o: *mut pyo3_ffi::PyObject) -> *mut pyo3_ffi::PyTypeObject {
210+
unsafe { (*o).ob_type }
211+
}
212+
213+
#[cfg(not(CPython))]
214+
#[inline(always)]
215+
#[allow(non_snake_case)]
216+
pub(crate) unsafe fn PyObject_Type(o: *mut pyo3_ffi::PyObject) -> *mut pyo3_ffi::PyTypeObject {
217+
unsafe { pyo3_ffi::Py_TYPE(o) }
218+
}
219+
206220
unsafe extern "C" {
207221

208222
#[cfg(CPython)]

src/ffi/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ mod pyboolref;
1212
#[cfg(all(CPython, not(Py_GIL_DISABLED)))]
1313
mod pybytearrayref;
1414
mod pybytesref;
15+
mod pydateref;
16+
mod pydatetimeref;
1517
mod pydictref;
1618
mod pyfloatref;
1719
mod pyfragmentref;
@@ -21,6 +23,7 @@ mod pylistref;
2123
mod pymemoryview;
2224
mod pynoneref;
2325
mod pystrref;
26+
mod pytimeref;
2427
mod pytupleref;
2528
mod pyuuidref;
2629
mod utf8;
@@ -33,13 +36,16 @@ pub(crate) use {
3336
fragment::{Fragment, orjson_fragmenttype_new},
3437
pyboolref::PyBoolRef,
3538
pybytesref::{PyBytesRef, PyBytesRefError},
39+
pydateref::PyDateRef,
40+
pydatetimeref::PyDateTimeRef,
3641
pydictref::PyDictRef,
3742
pyfloatref::PyFloatRef,
3843
pyfragmentref::{PyFragmentRef, PyFragmentRefError},
3944
pyintref::{PyIntError, PyIntKind, PyIntRef},
4045
pylistref::PyListRef,
4146
pynoneref::PyNoneRef,
4247
pystrref::{PyStrRef, PyStrSubclassRef, set_str_create_fn},
48+
pytimeref::PyTimeRef,
4349
pytupleref::PyTupleRef,
4450
pyuuidref::PyUuidRef,
4551
};

src/ffi/pydateref.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright ijl (2026)
3+
4+
use crate::ffi::{PyDateTime_GET_DAY, PyDateTime_GET_MONTH, PyDateTime_GET_YEAR, PyObject};
5+
6+
#[derive(Clone)]
7+
#[repr(transparent)]
8+
pub(crate) struct PyDateRef {
9+
ptr: core::ptr::NonNull<PyObject>,
10+
}
11+
12+
unsafe impl Send for PyDateRef {}
13+
unsafe impl Sync for PyDateRef {}
14+
15+
impl PartialEq for PyDateRef {
16+
fn eq(&self, other: &Self) -> bool {
17+
self.ptr == other.ptr
18+
}
19+
}
20+
21+
impl PyDateRef {
22+
#[inline]
23+
pub(crate) unsafe fn from_ptr_unchecked(ptr: *mut PyObject) -> Self {
24+
unsafe {
25+
debug_assert!(!ptr.is_null());
26+
debug_assert!(crate::ffi::PyObject_Type(ptr) == crate::typeref::DATE_TYPE);
27+
Self {
28+
ptr: core::ptr::NonNull::new_unchecked(ptr),
29+
}
30+
}
31+
}
32+
33+
#[inline]
34+
#[allow(unused)]
35+
pub fn as_ptr(&self) -> *mut PyObject {
36+
self.ptr.as_ptr()
37+
}
38+
39+
#[inline]
40+
#[allow(unused)]
41+
pub fn as_non_null_ptr(&self) -> core::ptr::NonNull<PyObject> {
42+
self.ptr
43+
}
44+
45+
#[inline]
46+
pub fn year(&self) -> u32 {
47+
unsafe {
48+
let tmp = PyDateTime_GET_YEAR(self.ptr.as_ptr());
49+
debug_assert!(tmp >= 0);
50+
#[allow(clippy::cast_sign_loss)]
51+
let val = tmp as u32;
52+
val
53+
}
54+
}
55+
56+
#[inline]
57+
pub fn month(&self) -> u32 {
58+
unsafe {
59+
let tmp = PyDateTime_GET_MONTH(self.ptr.as_ptr());
60+
debug_assert!(tmp >= 0);
61+
#[allow(clippy::cast_sign_loss)]
62+
let val = tmp as u32;
63+
val
64+
}
65+
}
66+
67+
#[inline]
68+
pub fn day(&self) -> u32 {
69+
unsafe {
70+
let tmp = PyDateTime_GET_DAY(self.ptr.as_ptr());
71+
debug_assert!(tmp >= 0);
72+
#[allow(clippy::cast_sign_loss)]
73+
let val = tmp as u32;
74+
val
75+
}
76+
}
77+
}

src/ffi/pydatetimeref.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
2+
// Copyright ijl (2025-2026), Ben Sully (2021)
3+
4+
use crate::typeref::{
5+
CONVERT_METHOD_STR, DST_STR, NORMALIZE_METHOD_STR, UTCOFFSET_METHOD_STR, ZONEINFO_TYPE,
6+
};
7+
8+
use crate::ffi::{PyObject_CallMethodNoArgs, PyObject_CallMethodOneArg, PyObject_HasAttr};
9+
10+
#[derive(Default)]
11+
pub(crate) struct Offset {
12+
pub day: i32,
13+
pub second: i32,
14+
}
15+
16+
#[derive(Clone)]
17+
#[repr(transparent)]
18+
pub(crate) struct PyDateTimeRef {
19+
ptr: core::ptr::NonNull<pyo3_ffi::PyObject>,
20+
}
21+
22+
unsafe impl Send for PyDateTimeRef {}
23+
unsafe impl Sync for PyDateTimeRef {}
24+
25+
impl PartialEq for PyDateTimeRef {
26+
fn eq(&self, other: &Self) -> bool {
27+
self.ptr == other.ptr
28+
}
29+
}
30+
31+
impl PyDateTimeRef {
32+
#[inline]
33+
pub(crate) unsafe fn from_ptr_unchecked(ptr: *mut pyo3_ffi::PyObject) -> Self {
34+
unsafe {
35+
debug_assert!(!ptr.is_null());
36+
debug_assert!(crate::ffi::PyObject_Type(ptr) == crate::typeref::DATETIME_TYPE);
37+
Self {
38+
ptr: core::ptr::NonNull::new_unchecked(ptr),
39+
}
40+
}
41+
}
42+
43+
#[inline]
44+
#[allow(unused)]
45+
pub fn as_ptr(&self) -> *mut pyo3_ffi::PyObject {
46+
self.ptr.as_ptr()
47+
}
48+
49+
#[inline]
50+
#[allow(unused)]
51+
pub fn as_non_null_ptr(&self) -> core::ptr::NonNull<pyo3_ffi::PyObject> {
52+
self.ptr
53+
}
54+
55+
#[inline]
56+
#[cfg(CPython)]
57+
pub fn tzinfo(&self) -> *mut crate::ffi::PyObject {
58+
unsafe {
59+
let ret = (*(self.ptr.as_ptr().cast::<crate::ffi::PyDateTime_DateTime>())).tzinfo;
60+
debug_assert!(!ret.is_null());
61+
ret
62+
}
63+
}
64+
65+
#[inline]
66+
#[cfg(not(CPython))]
67+
pub fn tzinfo(&self) -> *mut crate::ffi::PyObject {
68+
unsafe {
69+
let ret = crate::ffi::PyDateTime_DATE_GET_TZINFO(self.ptr.as_ptr());
70+
debug_assert!(!ret.is_null());
71+
ret
72+
}
73+
}
74+
75+
#[inline]
76+
#[cfg(CPython)]
77+
pub fn has_tz(&self) -> bool {
78+
unsafe { (*(self.ptr.as_ptr().cast::<crate::ffi::PyDateTime_DateTime>())).hastzinfo == 1 }
79+
}
80+
81+
#[inline]
82+
#[cfg(not(CPython))]
83+
pub fn has_tz(&self) -> bool {
84+
unsafe { self.tzinfo() != crate::typeref::NONE }
85+
}
86+
87+
#[inline]
88+
pub fn year(&self) -> i32 {
89+
unsafe { crate::ffi::PyDateTime_GET_YEAR(self.ptr.as_ptr()) }
90+
}
91+
92+
#[inline]
93+
pub fn month(&self) -> u8 {
94+
unsafe {
95+
let tmp = crate::ffi::PyDateTime_GET_MONTH(self.ptr.as_ptr());
96+
debug_assert!(tmp >= 0);
97+
#[allow(clippy::cast_sign_loss)]
98+
let val = tmp as u8;
99+
val
100+
}
101+
}
102+
103+
#[inline]
104+
pub fn day(&self) -> u8 {
105+
unsafe {
106+
let tmp = crate::ffi::PyDateTime_GET_DAY(self.ptr.as_ptr());
107+
debug_assert!(tmp >= 0);
108+
#[allow(clippy::cast_sign_loss)]
109+
let val = tmp as u8;
110+
val
111+
}
112+
}
113+
114+
#[inline]
115+
pub fn hour(&self) -> u8 {
116+
unsafe {
117+
let tmp = crate::ffi::PyDateTime_DATE_GET_HOUR(self.ptr.as_ptr());
118+
debug_assert!(tmp >= 0);
119+
#[allow(clippy::cast_sign_loss)]
120+
let val = tmp as u8;
121+
val
122+
}
123+
}
124+
125+
#[inline]
126+
pub fn minute(&self) -> u8 {
127+
unsafe {
128+
let tmp = crate::ffi::PyDateTime_DATE_GET_MINUTE(self.ptr.as_ptr());
129+
debug_assert!(tmp >= 0);
130+
#[allow(clippy::cast_sign_loss)]
131+
let val = tmp as u8;
132+
val
133+
}
134+
}
135+
136+
#[inline]
137+
pub fn second(&self) -> u8 {
138+
unsafe {
139+
let tmp = crate::ffi::PyDateTime_DATE_GET_SECOND(self.ptr.as_ptr());
140+
debug_assert!(tmp >= 0);
141+
#[allow(clippy::cast_sign_loss)]
142+
let val = tmp as u8;
143+
val
144+
}
145+
}
146+
147+
#[inline]
148+
pub fn microsecond(&self) -> u32 {
149+
unsafe {
150+
let tmp = crate::ffi::PyDateTime_DATE_GET_MICROSECOND(self.ptr.as_ptr());
151+
debug_assert!(tmp >= 0);
152+
#[allow(clippy::cast_sign_loss)]
153+
let val = tmp as u32;
154+
val
155+
}
156+
}
157+
#[cfg(not(CPython))]
158+
#[inline]
159+
pub fn offset(&self) -> Option<Offset> {
160+
unimplemented!()
161+
}
162+
163+
#[cfg(CPython)]
164+
#[inline]
165+
pub fn offset(&self) -> Option<Offset> {
166+
if !self.has_tz() {
167+
Some(Offset::default())
168+
} else {
169+
unsafe {
170+
let tzinfo = self.tzinfo();
171+
if core::ptr::eq(crate::ffi::PyObject_Type(tzinfo), ZONEINFO_TYPE) {
172+
// zoneinfo
173+
let py_offset =
174+
PyObject_CallMethodOneArg(tzinfo, UTCOFFSET_METHOD_STR, self.ptr.as_ptr());
175+
let offset = Offset {
176+
second: crate::ffi::PyDateTime_DELTA_GET_SECONDS(py_offset),
177+
day: crate::ffi::PyDateTime_DELTA_GET_DAYS(py_offset),
178+
};
179+
crate::ffi::Py_DECREF(py_offset);
180+
Some(offset)
181+
} else {
182+
self.slow_offset(tzinfo)
183+
}
184+
}
185+
}
186+
}
187+
188+
#[cfg(CPython)]
189+
#[cold]
190+
#[inline(never)]
191+
fn slow_offset(&self, tzinfo: *mut crate::ffi::PyObject) -> Option<Offset> {
192+
unsafe {
193+
if PyObject_HasAttr(tzinfo, CONVERT_METHOD_STR) == 1 {
194+
// pendulum
195+
let py_offset = PyObject_CallMethodNoArgs(self.ptr.as_ptr(), UTCOFFSET_METHOD_STR);
196+
let offset = Offset {
197+
second: crate::ffi::PyDateTime_DELTA_GET_SECONDS(py_offset),
198+
day: crate::ffi::PyDateTime_DELTA_GET_DAYS(py_offset),
199+
};
200+
crate::ffi::Py_DECREF(py_offset);
201+
Some(offset)
202+
} else if PyObject_HasAttr(tzinfo, NORMALIZE_METHOD_STR) == 1 {
203+
// pytz
204+
let method_ptr =
205+
PyObject_CallMethodOneArg(tzinfo, NORMALIZE_METHOD_STR, self.ptr.as_ptr());
206+
let py_offset = PyObject_CallMethodNoArgs(method_ptr, UTCOFFSET_METHOD_STR);
207+
crate::ffi::Py_DECREF(method_ptr);
208+
let offset = Offset {
209+
second: crate::ffi::PyDateTime_DELTA_GET_SECONDS(py_offset),
210+
day: crate::ffi::PyDateTime_DELTA_GET_DAYS(py_offset),
211+
};
212+
crate::ffi::Py_DECREF(py_offset);
213+
214+
Some(offset)
215+
} else if PyObject_HasAttr(tzinfo, DST_STR) == 1 {
216+
// dateutil/arrow, datetime.timezone.utc
217+
let py_offset =
218+
PyObject_CallMethodOneArg(tzinfo, UTCOFFSET_METHOD_STR, self.ptr.as_ptr());
219+
let offset = Offset {
220+
second: crate::ffi::PyDateTime_DELTA_GET_SECONDS(py_offset),
221+
day: crate::ffi::PyDateTime_DELTA_GET_DAYS(py_offset),
222+
};
223+
crate::ffi::Py_DECREF(py_offset);
224+
Some(offset)
225+
} else {
226+
None
227+
}
228+
}
229+
}
230+
}

0 commit comments

Comments
 (0)