Skip to content

Commit 04af5b1

Browse files
pganssleabalkin
authored andcommitted
bpo-10381: Add timezone to datetime C API (#5032)
* Add timezone to datetime C API * Add documentation for timezone C API macros * Add dedicated tests for datetime type check macros * Remove superfluous C API test * Drop support for TimeZoneType in datetime C API * Expose UTC singleton to the datetime C API * Update datetime C-API documentation to include links * Add reference count information for timezone constructors
1 parent ccbe581 commit 04af5b1

File tree

7 files changed

+329
-11
lines changed

7 files changed

+329
-11
lines changed

Doc/c-api/datetime.rst

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ the module initialisation function. The macro puts a pointer to a C structure
1313
into a static variable, :c:data:`PyDateTimeAPI`, that is used by the following
1414
macros.
1515

16+
Macro for access to the UTC singleton:
17+
18+
.. c:var:: PyObject* PyDateTime_TimeZone_UTC
19+
20+
Returns the time zone singleton representing UTC, the same object as
21+
:attr:`datetime.timezone.utc`.
22+
23+
.. versionadded:: 3.7
24+
25+
1626
Type-check macros:
1727

1828
.. c:function:: int PyDate_Check(PyObject *ob)
@@ -79,27 +89,41 @@ Macros to create objects:
7989
8090
.. c:function:: PyObject* PyDate_FromDate(int year, int month, int day)
8191
82-
Return a ``datetime.date`` object with the specified year, month and day.
92+
Return a :class:`datetime.date` object with the specified year, month and day.
8393
8494
8595
.. c:function:: PyObject* PyDateTime_FromDateAndTime(int year, int month, int day, int hour, int minute, int second, int usecond)
8696
87-
Return a ``datetime.datetime`` object with the specified year, month, day, hour,
97+
Return a :class:`datetime.datetime` object with the specified year, month, day, hour,
8898
minute, second and microsecond.
8999
90100
91101
.. c:function:: PyObject* PyTime_FromTime(int hour, int minute, int second, int usecond)
92102
93-
Return a ``datetime.time`` object with the specified hour, minute, second and
103+
Return a :class:`datetime.time` object with the specified hour, minute, second and
94104
microsecond.
95105
96106
97107
.. c:function:: PyObject* PyDelta_FromDSU(int days, int seconds, int useconds)
98108
99-
Return a ``datetime.timedelta`` object representing the given number of days,
100-
seconds and microseconds. Normalization is performed so that the resulting
101-
number of microseconds and seconds lie in the ranges documented for
102-
``datetime.timedelta`` objects.
109+
Return a :class:`datetime.timedelta` object representing the given number
110+
of days, seconds and microseconds. Normalization is performed so that the
111+
resulting number of microseconds and seconds lie in the ranges documented for
112+
:class:`datetime.timedelta` objects.
113+
114+
.. c:function:: PyObject* PyTimeZone_FromOffset(PyDateTime_DeltaType* offset)
115+
116+
Return a :class:`datetime.timezone` object with an unnamed fixed offset
117+
represented by the *offset* argument.
118+
119+
.. versionadded:: 3.7
120+
121+
.. c:function:: PyObject* PyTimeZone_FromOffsetAndName(PyDateTime_DeltaType* offset, PyUnicode* name)
122+
123+
Return a :class:`datetime.timezone` object with a fixed offset represented
124+
by the *offset* argument and with tzname *name*.
125+
126+
.. versionadded:: 3.7
103127
104128
105129
Macros to extract fields from date objects. The argument must be an instance of
@@ -199,11 +223,11 @@ Macros for the convenience of modules implementing the DB API:
199223
200224
.. c:function:: PyObject* PyDateTime_FromTimestamp(PyObject *args)
201225
202-
Create and return a new ``datetime.datetime`` object given an argument tuple
203-
suitable for passing to ``datetime.datetime.fromtimestamp()``.
226+
Create and return a new :class:`datetime.datetime` object given an argument
227+
tuple suitable for passing to :meth:`datetime.datetime.fromtimestamp()`.
204228
205229
206230
.. c:function:: PyObject* PyDate_FromTimestamp(PyObject *args)
207231
208-
Create and return a new ``datetime.date`` object given an argument tuple
209-
suitable for passing to ``datetime.date.fromtimestamp()``.
232+
Create and return a new :class:`datetime.date` object given an argument
233+
tuple suitable for passing to :meth:`datetime.date.fromtimestamp()`.

Doc/data/refcounts.dat

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ PyDelta_FromDSU:int:days::
177177
PyDelta_FromDSU:int:seconds::
178178
PyDelta_FromDSU:int:useconds::
179179

180+
PyTimeZone_FromOffset:PyObject*::+1:
181+
PyTimeZone_FromOffset:PyDateTime_DeltaType*:offset:+1:Reference count not increased if offset is +00:00
182+
183+
PyTimeZone_FromOffsetAndName:PyObject*::+1:
184+
PyTimeZone_FromOffsetAndName:PyDateTime_DeltaType*:offset:+1:Reference count not increased if offset is +00:00 and name == NULL
185+
PyTimeZone_FromOffsetAndName:PyUnicode*:name:+1:
186+
187+
180188
PyDescr_NewClassMethod:PyObject*::+1:
181189
PyDescr_NewClassMethod:PyTypeObject*:type::
182190
PyDescr_NewClassMethod:PyMethodDef*:method::

Include/datetime.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,16 @@ typedef struct {
155155
PyTypeObject *DeltaType;
156156
PyTypeObject *TZInfoType;
157157

158+
/* singletons */
159+
PyObject *TimeZone_UTC;
160+
158161
/* constructors */
159162
PyObject *(*Date_FromDate)(int, int, int, PyTypeObject*);
160163
PyObject *(*DateTime_FromDateAndTime)(int, int, int, int, int, int, int,
161164
PyObject*, PyTypeObject*);
162165
PyObject *(*Time_FromTime)(int, int, int, int, PyObject*, PyTypeObject*);
163166
PyObject *(*Delta_FromDelta)(int, int, int, int, PyTypeObject*);
167+
PyObject *(*TimeZone_FromTimeZone)(PyObject *offset, PyObject *name);
164168

165169
/* constructors for the DB API */
166170
PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*);
@@ -202,6 +206,9 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
202206
#define PyDateTime_IMPORT \
203207
PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0)
204208

209+
/* Macro for access to the UTC singleton */
210+
#define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC
211+
205212
/* Macros for type checking when not building the Python core. */
206213
#define PyDate_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->DateType)
207214
#define PyDate_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->DateType)
@@ -242,6 +249,12 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
242249
PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \
243250
PyDateTimeAPI->DeltaType)
244251

252+
#define PyTimeZone_FromOffset(offset) \
253+
PyDateTimeAPI->TimeZone_FromTimeZone(offset, NULL)
254+
255+
#define PyTimeZone_FromOffsetAndName(offset, name) \
256+
PyDateTimeAPI->TimeZone_FromTimeZone(offset, name)
257+
245258
/* Macros supporting the DB API. */
246259
#define PyDateTime_FromTimestamp(args) \
247260
PyDateTimeAPI->DateTime_FromTimestamp( \

Lib/test/datetimetester.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from datetime import date, datetime
3232
import time as _time
3333

34+
import _testcapi
35+
3436
# Needed by test_datetime
3537
import _strptime
3638
#
@@ -5443,6 +5445,185 @@ def __init__(self):
54435445
class IranTest(ZoneInfoTest):
54445446
zonename = 'Asia/Tehran'
54455447

5448+
5449+
class CapiTest(unittest.TestCase):
5450+
def setUp(self):
5451+
# Since the C API is not present in the _Pure tests, skip all tests
5452+
if self.__class__.__name__.endswith('Pure'):
5453+
self.skipTest('Not relevant in pure Python')
5454+
5455+
# This *must* be called, and it must be called first, so until either
5456+
# restriction is loosened, we'll call it as part of test setup
5457+
_testcapi.test_datetime_capi()
5458+
5459+
def test_utc_capi(self):
5460+
for use_macro in (True, False):
5461+
capi_utc = _testcapi.get_timezone_utc_capi(use_macro)
5462+
5463+
with self.subTest(use_macro=use_macro):
5464+
self.assertIs(capi_utc, timezone.utc)
5465+
5466+
def test_timezones_capi(self):
5467+
est_capi, est_macro, est_macro_nn = _testcapi.make_timezones_capi()
5468+
5469+
exp_named = timezone(timedelta(hours=-5), "EST")
5470+
exp_unnamed = timezone(timedelta(hours=-5))
5471+
5472+
cases = [
5473+
('est_capi', est_capi, exp_named),
5474+
('est_macro', est_macro, exp_named),
5475+
('est_macro_nn', est_macro_nn, exp_unnamed)
5476+
]
5477+
5478+
for name, tz_act, tz_exp in cases:
5479+
with self.subTest(name=name):
5480+
self.assertEqual(tz_act, tz_exp)
5481+
5482+
dt1 = datetime(2000, 2, 4, tzinfo=tz_act)
5483+
dt2 = datetime(2000, 2, 4, tzinfo=tz_exp)
5484+
5485+
self.assertEqual(dt1, dt2)
5486+
self.assertEqual(dt1.tzname(), dt2.tzname())
5487+
5488+
dt_utc = datetime(2000, 2, 4, 5, tzinfo=timezone.utc)
5489+
5490+
self.assertEqual(dt1.astimezone(timezone.utc), dt_utc)
5491+
5492+
def test_check_date(self):
5493+
class DateSubclass(date):
5494+
pass
5495+
5496+
d = date(2011, 1, 1)
5497+
ds = DateSubclass(2011, 1, 1)
5498+
dt = datetime(2011, 1, 1)
5499+
5500+
is_date = _testcapi.datetime_check_date
5501+
5502+
# Check the ones that should be valid
5503+
self.assertTrue(is_date(d))
5504+
self.assertTrue(is_date(dt))
5505+
self.assertTrue(is_date(ds))
5506+
self.assertTrue(is_date(d, True))
5507+
5508+
# Check that the subclasses do not match exactly
5509+
self.assertFalse(is_date(dt, True))
5510+
self.assertFalse(is_date(ds, True))
5511+
5512+
# Check that various other things are not dates at all
5513+
args = [tuple(), list(), 1, '2011-01-01',
5514+
timedelta(1), timezone.utc, time(12, 00)]
5515+
for arg in args:
5516+
for exact in (True, False):
5517+
with self.subTest(arg=arg, exact=exact):
5518+
self.assertFalse(is_date(arg, exact))
5519+
5520+
def test_check_time(self):
5521+
class TimeSubclass(time):
5522+
pass
5523+
5524+
t = time(12, 30)
5525+
ts = TimeSubclass(12, 30)
5526+
5527+
is_time = _testcapi.datetime_check_time
5528+
5529+
# Check the ones that should be valid
5530+
self.assertTrue(is_time(t))
5531+
self.assertTrue(is_time(ts))
5532+
self.assertTrue(is_time(t, True))
5533+
5534+
# Check that the subclass does not match exactly
5535+
self.assertFalse(is_time(ts, True))
5536+
5537+
# Check that various other things are not times
5538+
args = [tuple(), list(), 1, '2011-01-01',
5539+
timedelta(1), timezone.utc, date(2011, 1, 1)]
5540+
5541+
for arg in args:
5542+
for exact in (True, False):
5543+
with self.subTest(arg=arg, exact=exact):
5544+
self.assertFalse(is_time(arg, exact))
5545+
5546+
def test_check_datetime(self):
5547+
class DateTimeSubclass(datetime):
5548+
pass
5549+
5550+
dt = datetime(2011, 1, 1, 12, 30)
5551+
dts = DateTimeSubclass(2011, 1, 1, 12, 30)
5552+
5553+
is_datetime = _testcapi.datetime_check_datetime
5554+
5555+
# Check the ones that should be valid
5556+
self.assertTrue(is_datetime(dt))
5557+
self.assertTrue(is_datetime(dts))
5558+
self.assertTrue(is_datetime(dt, True))
5559+
5560+
# Check that the subclass does not match exactly
5561+
self.assertFalse(is_datetime(dts, True))
5562+
5563+
# Check that various other things are not datetimes
5564+
args = [tuple(), list(), 1, '2011-01-01',
5565+
timedelta(1), timezone.utc, date(2011, 1, 1)]
5566+
5567+
for arg in args:
5568+
for exact in (True, False):
5569+
with self.subTest(arg=arg, exact=exact):
5570+
self.assertFalse(is_datetime(arg, exact))
5571+
5572+
def test_check_delta(self):
5573+
class TimeDeltaSubclass(timedelta):
5574+
pass
5575+
5576+
td = timedelta(1)
5577+
tds = TimeDeltaSubclass(1)
5578+
5579+
is_timedelta = _testcapi.datetime_check_delta
5580+
5581+
# Check the ones that should be valid
5582+
self.assertTrue(is_timedelta(td))
5583+
self.assertTrue(is_timedelta(tds))
5584+
self.assertTrue(is_timedelta(td, True))
5585+
5586+
# Check that the subclass does not match exactly
5587+
self.assertFalse(is_timedelta(tds, True))
5588+
5589+
# Check that various other things are not timedeltas
5590+
args = [tuple(), list(), 1, '2011-01-01',
5591+
timezone.utc, date(2011, 1, 1), datetime(2011, 1, 1)]
5592+
5593+
for arg in args:
5594+
for exact in (True, False):
5595+
with self.subTest(arg=arg, exact=exact):
5596+
self.assertFalse(is_timedelta(arg, exact))
5597+
5598+
def test_check_tzinfo(self):
5599+
class TZInfoSubclass(tzinfo):
5600+
pass
5601+
5602+
tzi = tzinfo()
5603+
tzis = TZInfoSubclass()
5604+
tz = timezone(timedelta(hours=-5))
5605+
5606+
is_tzinfo = _testcapi.datetime_check_tzinfo
5607+
5608+
# Check the ones that should be valid
5609+
self.assertTrue(is_tzinfo(tzi))
5610+
self.assertTrue(is_tzinfo(tz))
5611+
self.assertTrue(is_tzinfo(tzis))
5612+
self.assertTrue(is_tzinfo(tzi, True))
5613+
5614+
# Check that the subclasses do not match exactly
5615+
self.assertFalse(is_tzinfo(tz, True))
5616+
self.assertFalse(is_tzinfo(tzis, True))
5617+
5618+
# Check that various other things are not tzinfos
5619+
args = [tuple(), list(), 1, '2011-01-01',
5620+
date(2011, 1, 1), datetime(2011, 1, 1)]
5621+
5622+
for arg in args:
5623+
for exact in (True, False):
5624+
with self.subTest(arg=arg, exact=exact):
5625+
self.assertFalse(is_tzinfo(arg, exact))
5626+
54465627
def load_tests(loader, standard_tests, pattern):
54475628
standard_tests.addTest(ZoneInfoCompleteTest())
54485629
return standard_tests
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add C API access to the ``datetime.timezone`` constructor and
2+
``datetime.timzone.UTC`` singleton.

Modules/_datetimemodule.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6036,10 +6036,12 @@ static PyDateTime_CAPI CAPI = {
60366036
&PyDateTime_TimeType,
60376037
&PyDateTime_DeltaType,
60386038
&PyDateTime_TZInfoType,
6039+
NULL, // PyDatetime_TimeZone_UTC not initialized yet
60396040
new_date_ex,
60406041
new_datetime_ex,
60416042
new_time_ex,
60426043
new_delta_ex,
6044+
new_timezone,
60436045
datetime_fromtimestamp,
60446046
date_fromtimestamp,
60456047
new_datetime_ex2,
@@ -6168,6 +6170,7 @@ PyInit__datetime(void)
61686170
if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0)
61696171
return NULL;
61706172
PyDateTime_TimeZone_UTC = x;
6173+
CAPI.TimeZone_UTC = PyDateTime_TimeZone_UTC;
61716174

61726175
delta = new_delta(-1, 60, 0, 1); /* -23:59 */
61736176
if (delta == NULL)

0 commit comments

Comments
 (0)