Skip to content

Commit 711b9e8

Browse files
committed
datetime.datetime may be timezone-naive
1 parent 06f7e43 commit 711b9e8

3 files changed

Lines changed: 144 additions & 114 deletions

File tree

README.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,40 +168,65 @@ orjson serializes `datetime.datetime` objects to
168168
[RFC 3339](https://tools.ietf.org/html/rfc3339) format, a subset of
169169
ISO 8601.
170170

171-
`datetime.datetime` objects must have `tzinfo` set. For UTC timezones,
172-
`datetime.timezone.utc` is sufficient. For other timezones, `tzinfo`
173-
must be a timezone object from the pendulum, pytz, or dateutil libraries. For
174-
applications in which naive datetimes are known to be UTC, `tzinfo` may be
175-
omitted if `orjson.OPT_NAIVE_UTC` if specified. This does not affect
176-
datetimes with a `tzinfo` set.
171+
`datetime.datetime` objects serialize with or without a `tzinfo`. For a full
172+
RFC 3339 representation, `tzinfo` must be present or `orjson.OPT_NAIVE_UTC`
173+
must be specified (e.g., for timestamps stored in a database in UTC and
174+
deserialized by the database adapter without a `tzinfo`). If a
175+
`tzinfo` is not present, a timezone offset is not serialized.
176+
177+
`tzinfo`, if specified, must be a timezone object that is either
178+
`datetime.timezone.utc` or from the `pendulum`, `pytz`, or
179+
`dateutil`/`arrow` libraries.
177180

178181
```python
179182
>>> import orjson, datetime, pendulum
180183
>>> orjson.dumps(
181-
datetime.datetime.fromtimestamp(4123518902), option=orjson.OPT_NAIVE_UTC
184+
datetime.datetime.fromtimestamp(4123518902).replace(tzinfo=datetime.timezone.utc)
182185
)
183186
b'"2100-09-01T21:55:02+00:00"'
184187
>>> orjson.dumps(
185-
datetime.datetime.fromtimestamp(4123518902).replace(tzinfo=datetime.timezone.utc)
188+
datetime.datetime(2018, 12, 1, 2, 3, 4, 9, tzinfo=pendulum.timezone('Australia/Adelaide'))
189+
)
190+
b'"2018-12-01T02:03:04.9+10:30"'
191+
>>> orjson.dumps(
192+
datetime.datetime.fromtimestamp(4123518902)
193+
)
194+
b'"2100-09-01T21:55:02"'
195+
```
196+
197+
`orjson.OPT_NAIVE_UTC`, if specified, only applies to objects that do not have
198+
a `tzinfo`.
199+
200+
```python
201+
>>> import orjson, datetime, pendulum
202+
>>> orjson.dumps(
203+
datetime.datetime.fromtimestamp(4123518902),
204+
option=orjson.OPT_NAIVE_UTC
186205
)
187206
b'"2100-09-01T21:55:02+00:00"'
188207
>>> orjson.dumps(
189-
datetime.datetime(2018, 12, 1, 2, 3, 4, 9, tzinfo=pendulum.timezone('Australia/Adelaide'))
208+
datetime.datetime(2018, 12, 1, 2, 3, 4, 9, tzinfo=pendulum.timezone('Australia/Adelaide')),
209+
option=orjson.OPT_NAIVE_UTC
190210
)
191211
b'"2018-12-01T02:03:04.9+10:30"'
192212
```
193213

194-
`datetime.time` objects must not have a `tzinfo`. `datetime.date` objects
195-
will always serialize.
214+
`datetime.time` objects must not have a `tzinfo`.
196215

197216
```python
198217
>>> import orjson, datetime
199-
>>> orjson.dumps(datetime.date(1900, 1, 2))
200-
b'"1900-01-02"'
201218
>>> orjson.dumps(datetime.time(12, 0, 15, 291290))
202219
b'"12:00:15.291290"'
203220
```
204221

222+
`datetime.date` objects will always serialize.
223+
224+
```python
225+
>>> import orjson, datetime
226+
>>> orjson.dumps(datetime.date(1900, 1, 2))
227+
b'"1900-01-02"'
228+
```
229+
205230
Errors with `tzinfo` result in `JSONEncodeError` being raised.
206231

207232
It is faster to have orjson serialize datetime objects than to do so

src/encode.rs

Lines changed: 100 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -178,76 +178,77 @@ impl<'p> Serialize for SerializePyObject {
178178
serializer.serialize_seq(None).unwrap().end()
179179
}
180180
} else if unsafe { obj_ptr == DATETIME_PTR } {
181-
if unsafe {
182-
(*(self.ptr as *mut pyo3::ffi::PyDateTime_DateTime)).hastzinfo == 0
183-
&& !self.opts & NAIVE_UTC == NAIVE_UTC
184-
} {
185-
return Err(ser::Error::custom(
186-
"datetime.datetime must have tzinfo set; use datetime.timezone.utc if UTC",
187-
));
188-
}
189-
let tzinfo = unsafe { pyo3::ffi::PyDateTime_DATE_GET_TZINFO(self.ptr) };
181+
let has_tz =
182+
unsafe { (*(self.ptr as *mut pyo3::ffi::PyDateTime_DateTime)).hastzinfo == 1 };
190183
let offset_day: i32;
191184
let mut offset_second: i32;
192-
if unsafe { (*(self.ptr as *mut pyo3::ffi::PyDateTime_DateTime)).hastzinfo == 1 } {
193-
if unsafe { pyo3::ffi::PyObject_HasAttr(tzinfo, CONVERT_METHOD_STR) == 1 } {
194-
// pendulum
195-
let offset = unsafe {
196-
pyo3::ffi::PyObject_CallMethodObjArgs(
197-
self.ptr,
198-
UTCOFFSET_METHOD_STR,
199-
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
200-
)
201-
};
202-
// test_datetime_partial_second_pendulum_not_supported
203-
if offset.is_null() {
204-
return Err(ser::Error::custom(
205-
"datetime does not support timezones with offsets that are not even minutes",
206-
));
207-
}
208-
offset_second =
209-
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
210-
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
211-
} else if unsafe {
212-
pyo3::ffi::PyObject_HasAttr(tzinfo, NORMALIZE_METHOD_STR) == 1
185+
if !has_tz {
186+
offset_second = 0;
187+
offset_day = 0;
188+
} else {
189+
let tzinfo = unsafe { pyo3::ffi::PyDateTime_DATE_GET_TZINFO(self.ptr) };
190+
if unsafe {
191+
(*(self.ptr as *mut pyo3::ffi::PyDateTime_DateTime)).hastzinfo == 1
213192
} {
214-
// pytz
215-
let offset = unsafe {
216-
pyo3::ffi::PyObject_CallMethodObjArgs(
193+
if unsafe { pyo3::ffi::PyObject_HasAttr(tzinfo, CONVERT_METHOD_STR) == 1 } {
194+
// pendulum
195+
let offset = unsafe {
196+
pyo3::ffi::PyObject_CallMethodObjArgs(
197+
self.ptr,
198+
UTCOFFSET_METHOD_STR,
199+
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
200+
)
201+
};
202+
// test_datetime_partial_second_pendulum_not_supported
203+
if offset.is_null() {
204+
return Err(ser::Error::custom(
205+
"datetime does not support timezones with offsets that are not even minutes",
206+
));
207+
}
208+
offset_second =
209+
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
210+
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
211+
} else if unsafe {
212+
pyo3::ffi::PyObject_HasAttr(tzinfo, NORMALIZE_METHOD_STR) == 1
213+
} {
214+
// pytz
215+
let offset = unsafe {
216+
pyo3::ffi::PyObject_CallMethodObjArgs(
217+
pyo3::ffi::PyObject_CallMethodObjArgs(
218+
tzinfo,
219+
NORMALIZE_METHOD_STR,
220+
self.ptr,
221+
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
222+
),
223+
UTCOFFSET_METHOD_STR,
224+
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
225+
)
226+
};
227+
offset_second =
228+
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
229+
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
230+
} else if unsafe { pyo3::ffi::PyObject_HasAttr(tzinfo, DST_STR) == 1 } {
231+
// dateutil/arrow, datetime.timezone.utc
232+
let offset = unsafe {
217233
pyo3::ffi::PyObject_CallMethodObjArgs(
218234
tzinfo,
219-
NORMALIZE_METHOD_STR,
235+
UTCOFFSET_METHOD_STR,
220236
self.ptr,
221237
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
222-
),
223-
UTCOFFSET_METHOD_STR,
224-
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
225-
)
226-
};
227-
offset_second =
228-
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
229-
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
230-
} else if unsafe { pyo3::ffi::PyObject_HasAttr(tzinfo, DST_STR) == 1 } {
231-
// dateutil/arrow, datetime.timezone.utc
232-
let offset = unsafe {
233-
pyo3::ffi::PyObject_CallMethodObjArgs(
234-
tzinfo,
235-
UTCOFFSET_METHOD_STR,
236-
self.ptr,
237-
std::ptr::null_mut() as *mut pyo3::ffi::PyObject,
238-
)
239-
};
240-
offset_second =
241-
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
242-
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
238+
)
239+
};
240+
offset_second =
241+
unsafe { pyo3::ffi::PyDateTime_DELTA_GET_SECONDS(offset) as i32 };
242+
offset_day = unsafe { pyo3::ffi::PyDateTime_DELTA_GET_DAYS(offset) };
243+
} else {
244+
return Err(ser::Error::custom(
245+
"datetime's timezone library is not supported: use datetime.timezone.utc, pendulum, pytz, or dateutil",
246+
));
247+
}
243248
} else {
244-
return Err(ser::Error::custom(
245-
"datetime's timezone library is not supported: use datetime.timezone.utc, pendulum, pytz, or dateutil",
246-
));
249+
offset_second = 0;
250+
offset_day = 0;
247251
}
248-
} else {
249-
offset_second = 0;
250-
offset_day = 0;
251252
};
252253

253254
let mut dt: SmallVec<[u8; 32]> = SmallVec::with_capacity(32);
@@ -316,49 +317,51 @@ impl<'p> Serialize for SerializePyObject {
316317
dt.extend(itoa::Buffer::new().format(microsecond).bytes());
317318
}
318319
}
319-
if offset_second == 0 {
320-
dt.push(PLUS);
321-
dt.push(ZERO);
322-
dt.push(ZERO);
323-
dt.push(COLON);
324-
dt.push(ZERO);
325-
dt.push(ZERO);
326-
} else {
327-
if offset_day == -1 {
328-
// datetime.timedelta(days=-1, seconds=68400) -> -05:00
329-
dt.push(HYPHEN);
330-
offset_second = 86400 - offset_second
331-
} else {
332-
// datetime.timedelta(seconds=37800) -> +10:30
320+
if has_tz || self.opts & NAIVE_UTC == NAIVE_UTC {
321+
if offset_second == 0 {
333322
dt.push(PLUS);
334-
}
335-
{
336-
let offset_minute = offset_second / 60;
337-
let offset_hour = offset_minute / 60;
338-
if offset_hour < 10 {
339-
dt.push(ZERO);
340-
}
341-
dt.extend(itoa::Buffer::new().format(offset_hour).bytes());
323+
dt.push(ZERO);
324+
dt.push(ZERO);
342325
dt.push(COLON);
326+
dt.push(ZERO);
327+
dt.push(ZERO);
328+
} else {
329+
if offset_day == -1 {
330+
// datetime.timedelta(days=-1, seconds=68400) -> -05:00
331+
dt.push(HYPHEN);
332+
offset_second = 86400 - offset_second
333+
} else {
334+
// datetime.timedelta(seconds=37800) -> +10:30
335+
dt.push(PLUS);
336+
}
337+
{
338+
let offset_minute = offset_second / 60;
339+
let offset_hour = offset_minute / 60;
340+
if offset_hour < 10 {
341+
dt.push(ZERO);
342+
}
343+
dt.extend(itoa::Buffer::new().format(offset_hour).bytes());
344+
dt.push(COLON);
343345

344-
let mut offset_minute_print = offset_minute % 60;
346+
let mut offset_minute_print = offset_minute % 60;
345347

346-
{
347-
// https://tools.ietf.org/html/rfc3339#section-5.8
348-
// "exactly 19 minutes and 32.13 seconds ahead of UTC"
349-
// "closest representable UTC offset"
350-
// "+20:00"
351-
let offset_excess_second =
352-
offset_second - (offset_minute_print * 60 + offset_hour * 3600);
353-
if offset_excess_second >= 30 {
354-
offset_minute_print += 1;
348+
{
349+
// https://tools.ietf.org/html/rfc3339#section-5.8
350+
// "exactly 19 minutes and 32.13 seconds ahead of UTC"
351+
// "closest representable UTC offset"
352+
// "+20:00"
353+
let offset_excess_second =
354+
offset_second - (offset_minute_print * 60 + offset_hour * 3600);
355+
if offset_excess_second >= 30 {
356+
offset_minute_print += 1;
357+
}
355358
}
356-
}
357359

358-
if offset_minute_print < 10 {
359-
dt.push(ZERO);
360+
if offset_minute_print < 10 {
361+
dt.push(ZERO);
362+
}
363+
dt.extend(itoa::Buffer::new().format(offset_minute_print).bytes());
360364
}
361-
dt.extend(itoa::Buffer::new().format(offset_minute_print).bytes());
362365
}
363366
}
364367
serializer.serialize_str(unsafe {

test/test_datetime.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ class DatetimeTests(unittest.TestCase):
1616

1717
def test_datetime_naive(self):
1818
"""
19-
datetime.datetime naive TypeError
19+
datetime.datetime naive prints without offset
2020
"""
21-
with self.assertRaises(TypeError):
22-
orjson.dumps([datetime.datetime(2000, 1, 1, 2, 3, 4, 123)])
21+
self.assertEqual(
22+
orjson.dumps([datetime.datetime(2000, 1, 1, 2, 3, 4, 123)]),
23+
b'["2000-01-01T02:03:04.123"]'
24+
)
2325

2426
def test_datetime_naive_utc(self):
2527
"""
@@ -251,4 +253,4 @@ def test_time_tz(self):
251253
datetime.time with tzinfo error
252254
"""
253255
with self.assertRaises(orjson.JSONEncodeError):
254-
orjson.dumps([datetime.time(12, 15, 59, 111, tzinfo=tz.gettz('Asia/Shanghai'))]),
256+
orjson.dumps([datetime.time(12, 15, 59, 111, tzinfo=tz.gettz('Asia/Shanghai'))])

0 commit comments

Comments
 (0)