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
This commit is contained in:
Paul Ganssle 2018-01-24 17:29:30 -05:00 committed by Alexander Belopolsky
parent ccbe5818af
commit 04af5b1ba9
7 changed files with 329 additions and 11 deletions

View File

@ -13,6 +13,16 @@ the module initialisation function. The macro puts a pointer to a C structure
into a static variable, :c:data:`PyDateTimeAPI`, that is used by the following into a static variable, :c:data:`PyDateTimeAPI`, that is used by the following
macros. macros.
Macro for access to the UTC singleton:
.. c:var:: PyObject* PyDateTime_TimeZone_UTC
Returns the time zone singleton representing UTC, the same object as
:attr:`datetime.timezone.utc`.
.. versionadded:: 3.7
Type-check macros: Type-check macros:
.. c:function:: int PyDate_Check(PyObject *ob) .. c:function:: int PyDate_Check(PyObject *ob)
@ -79,27 +89,41 @@ Macros to create objects:
.. c:function:: PyObject* PyDate_FromDate(int year, int month, int day) .. c:function:: PyObject* PyDate_FromDate(int year, int month, int day)
Return a ``datetime.date`` object with the specified year, month and day. Return a :class:`datetime.date` object with the specified year, month and day.
.. c:function:: PyObject* PyDateTime_FromDateAndTime(int year, int month, int day, int hour, int minute, int second, int usecond) .. c:function:: PyObject* PyDateTime_FromDateAndTime(int year, int month, int day, int hour, int minute, int second, int usecond)
Return a ``datetime.datetime`` object with the specified year, month, day, hour, Return a :class:`datetime.datetime` object with the specified year, month, day, hour,
minute, second and microsecond. minute, second and microsecond.
.. c:function:: PyObject* PyTime_FromTime(int hour, int minute, int second, int usecond) .. c:function:: PyObject* PyTime_FromTime(int hour, int minute, int second, int usecond)
Return a ``datetime.time`` object with the specified hour, minute, second and Return a :class:`datetime.time` object with the specified hour, minute, second and
microsecond. microsecond.
.. c:function:: PyObject* PyDelta_FromDSU(int days, int seconds, int useconds) .. c:function:: PyObject* PyDelta_FromDSU(int days, int seconds, int useconds)
Return a ``datetime.timedelta`` object representing the given number of days, Return a :class:`datetime.timedelta` object representing the given number
seconds and microseconds. Normalization is performed so that the resulting of days, seconds and microseconds. Normalization is performed so that the
number of microseconds and seconds lie in the ranges documented for resulting number of microseconds and seconds lie in the ranges documented for
``datetime.timedelta`` objects. :class:`datetime.timedelta` objects.
.. c:function:: PyObject* PyTimeZone_FromOffset(PyDateTime_DeltaType* offset)
Return a :class:`datetime.timezone` object with an unnamed fixed offset
represented by the *offset* argument.
.. versionadded:: 3.7
.. c:function:: PyObject* PyTimeZone_FromOffsetAndName(PyDateTime_DeltaType* offset, PyUnicode* name)
Return a :class:`datetime.timezone` object with a fixed offset represented
by the *offset* argument and with tzname *name*.
.. versionadded:: 3.7
Macros to extract fields from date objects. The argument must be an instance of 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:
.. c:function:: PyObject* PyDateTime_FromTimestamp(PyObject *args) .. c:function:: PyObject* PyDateTime_FromTimestamp(PyObject *args)
Create and return a new ``datetime.datetime`` object given an argument tuple Create and return a new :class:`datetime.datetime` object given an argument
suitable for passing to ``datetime.datetime.fromtimestamp()``. tuple suitable for passing to :meth:`datetime.datetime.fromtimestamp()`.
.. c:function:: PyObject* PyDate_FromTimestamp(PyObject *args) .. c:function:: PyObject* PyDate_FromTimestamp(PyObject *args)
Create and return a new ``datetime.date`` object given an argument tuple Create and return a new :class:`datetime.date` object given an argument
suitable for passing to ``datetime.date.fromtimestamp()``. tuple suitable for passing to :meth:`datetime.date.fromtimestamp()`.

View File

@ -177,6 +177,14 @@ PyDelta_FromDSU:int:days::
PyDelta_FromDSU:int:seconds:: PyDelta_FromDSU:int:seconds::
PyDelta_FromDSU:int:useconds:: PyDelta_FromDSU:int:useconds::
PyTimeZone_FromOffset:PyObject*::+1:
PyTimeZone_FromOffset:PyDateTime_DeltaType*:offset:+1:Reference count not increased if offset is +00:00
PyTimeZone_FromOffsetAndName:PyObject*::+1:
PyTimeZone_FromOffsetAndName:PyDateTime_DeltaType*:offset:+1:Reference count not increased if offset is +00:00 and name == NULL
PyTimeZone_FromOffsetAndName:PyUnicode*:name:+1:
PyDescr_NewClassMethod:PyObject*::+1: PyDescr_NewClassMethod:PyObject*::+1:
PyDescr_NewClassMethod:PyTypeObject*:type:: PyDescr_NewClassMethod:PyTypeObject*:type::
PyDescr_NewClassMethod:PyMethodDef*:method:: PyDescr_NewClassMethod:PyMethodDef*:method::

View File

@ -155,12 +155,16 @@ typedef struct {
PyTypeObject *DeltaType; PyTypeObject *DeltaType;
PyTypeObject *TZInfoType; PyTypeObject *TZInfoType;
/* singletons */
PyObject *TimeZone_UTC;
/* constructors */ /* constructors */
PyObject *(*Date_FromDate)(int, int, int, PyTypeObject*); PyObject *(*Date_FromDate)(int, int, int, PyTypeObject*);
PyObject *(*DateTime_FromDateAndTime)(int, int, int, int, int, int, int, PyObject *(*DateTime_FromDateAndTime)(int, int, int, int, int, int, int,
PyObject*, PyTypeObject*); PyObject*, PyTypeObject*);
PyObject *(*Time_FromTime)(int, int, int, int, PyObject*, PyTypeObject*); PyObject *(*Time_FromTime)(int, int, int, int, PyObject*, PyTypeObject*);
PyObject *(*Delta_FromDelta)(int, int, int, int, PyTypeObject*); PyObject *(*Delta_FromDelta)(int, int, int, int, PyTypeObject*);
PyObject *(*TimeZone_FromTimeZone)(PyObject *offset, PyObject *name);
/* constructors for the DB API */ /* constructors for the DB API */
PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*); PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*);
@ -202,6 +206,9 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
#define PyDateTime_IMPORT \ #define PyDateTime_IMPORT \
PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0) PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0)
/* Macro for access to the UTC singleton */
#define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC
/* Macros for type checking when not building the Python core. */ /* Macros for type checking when not building the Python core. */
#define PyDate_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->DateType) #define PyDate_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->DateType)
#define PyDate_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->DateType) #define PyDate_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->DateType)
@ -242,6 +249,12 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \ PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \
PyDateTimeAPI->DeltaType) PyDateTimeAPI->DeltaType)
#define PyTimeZone_FromOffset(offset) \
PyDateTimeAPI->TimeZone_FromTimeZone(offset, NULL)
#define PyTimeZone_FromOffsetAndName(offset, name) \
PyDateTimeAPI->TimeZone_FromTimeZone(offset, name)
/* Macros supporting the DB API. */ /* Macros supporting the DB API. */
#define PyDateTime_FromTimestamp(args) \ #define PyDateTime_FromTimestamp(args) \
PyDateTimeAPI->DateTime_FromTimestamp( \ PyDateTimeAPI->DateTime_FromTimestamp( \

View File

@ -31,6 +31,8 @@ from datetime import timezone
from datetime import date, datetime from datetime import date, datetime
import time as _time import time as _time
import _testcapi
# Needed by test_datetime # Needed by test_datetime
import _strptime import _strptime
# #
@ -5443,6 +5445,185 @@ class ZoneInfoCompleteTest(unittest.TestSuite):
class IranTest(ZoneInfoTest): class IranTest(ZoneInfoTest):
zonename = 'Asia/Tehran' zonename = 'Asia/Tehran'
class CapiTest(unittest.TestCase):
def setUp(self):
# Since the C API is not present in the _Pure tests, skip all tests
if self.__class__.__name__.endswith('Pure'):
self.skipTest('Not relevant in pure Python')
# This *must* be called, and it must be called first, so until either
# restriction is loosened, we'll call it as part of test setup
_testcapi.test_datetime_capi()
def test_utc_capi(self):
for use_macro in (True, False):
capi_utc = _testcapi.get_timezone_utc_capi(use_macro)
with self.subTest(use_macro=use_macro):
self.assertIs(capi_utc, timezone.utc)
def test_timezones_capi(self):
est_capi, est_macro, est_macro_nn = _testcapi.make_timezones_capi()
exp_named = timezone(timedelta(hours=-5), "EST")
exp_unnamed = timezone(timedelta(hours=-5))
cases = [
('est_capi', est_capi, exp_named),
('est_macro', est_macro, exp_named),
('est_macro_nn', est_macro_nn, exp_unnamed)
]
for name, tz_act, tz_exp in cases:
with self.subTest(name=name):
self.assertEqual(tz_act, tz_exp)
dt1 = datetime(2000, 2, 4, tzinfo=tz_act)
dt2 = datetime(2000, 2, 4, tzinfo=tz_exp)
self.assertEqual(dt1, dt2)
self.assertEqual(dt1.tzname(), dt2.tzname())
dt_utc = datetime(2000, 2, 4, 5, tzinfo=timezone.utc)
self.assertEqual(dt1.astimezone(timezone.utc), dt_utc)
def test_check_date(self):
class DateSubclass(date):
pass
d = date(2011, 1, 1)
ds = DateSubclass(2011, 1, 1)
dt = datetime(2011, 1, 1)
is_date = _testcapi.datetime_check_date
# Check the ones that should be valid
self.assertTrue(is_date(d))
self.assertTrue(is_date(dt))
self.assertTrue(is_date(ds))
self.assertTrue(is_date(d, True))
# Check that the subclasses do not match exactly
self.assertFalse(is_date(dt, True))
self.assertFalse(is_date(ds, True))
# Check that various other things are not dates at all
args = [tuple(), list(), 1, '2011-01-01',
timedelta(1), timezone.utc, time(12, 00)]
for arg in args:
for exact in (True, False):
with self.subTest(arg=arg, exact=exact):
self.assertFalse(is_date(arg, exact))
def test_check_time(self):
class TimeSubclass(time):
pass
t = time(12, 30)
ts = TimeSubclass(12, 30)
is_time = _testcapi.datetime_check_time
# Check the ones that should be valid
self.assertTrue(is_time(t))
self.assertTrue(is_time(ts))
self.assertTrue(is_time(t, True))
# Check that the subclass does not match exactly
self.assertFalse(is_time(ts, True))
# Check that various other things are not times
args = [tuple(), list(), 1, '2011-01-01',
timedelta(1), timezone.utc, date(2011, 1, 1)]
for arg in args:
for exact in (True, False):
with self.subTest(arg=arg, exact=exact):
self.assertFalse(is_time(arg, exact))
def test_check_datetime(self):
class DateTimeSubclass(datetime):
pass
dt = datetime(2011, 1, 1, 12, 30)
dts = DateTimeSubclass(2011, 1, 1, 12, 30)
is_datetime = _testcapi.datetime_check_datetime
# Check the ones that should be valid
self.assertTrue(is_datetime(dt))
self.assertTrue(is_datetime(dts))
self.assertTrue(is_datetime(dt, True))
# Check that the subclass does not match exactly
self.assertFalse(is_datetime(dts, True))
# Check that various other things are not datetimes
args = [tuple(), list(), 1, '2011-01-01',
timedelta(1), timezone.utc, date(2011, 1, 1)]
for arg in args:
for exact in (True, False):
with self.subTest(arg=arg, exact=exact):
self.assertFalse(is_datetime(arg, exact))
def test_check_delta(self):
class TimeDeltaSubclass(timedelta):
pass
td = timedelta(1)
tds = TimeDeltaSubclass(1)
is_timedelta = _testcapi.datetime_check_delta
# Check the ones that should be valid
self.assertTrue(is_timedelta(td))
self.assertTrue(is_timedelta(tds))
self.assertTrue(is_timedelta(td, True))
# Check that the subclass does not match exactly
self.assertFalse(is_timedelta(tds, True))
# Check that various other things are not timedeltas
args = [tuple(), list(), 1, '2011-01-01',
timezone.utc, date(2011, 1, 1), datetime(2011, 1, 1)]
for arg in args:
for exact in (True, False):
with self.subTest(arg=arg, exact=exact):
self.assertFalse(is_timedelta(arg, exact))
def test_check_tzinfo(self):
class TZInfoSubclass(tzinfo):
pass
tzi = tzinfo()
tzis = TZInfoSubclass()
tz = timezone(timedelta(hours=-5))
is_tzinfo = _testcapi.datetime_check_tzinfo
# Check the ones that should be valid
self.assertTrue(is_tzinfo(tzi))
self.assertTrue(is_tzinfo(tz))
self.assertTrue(is_tzinfo(tzis))
self.assertTrue(is_tzinfo(tzi, True))
# Check that the subclasses do not match exactly
self.assertFalse(is_tzinfo(tz, True))
self.assertFalse(is_tzinfo(tzis, True))
# Check that various other things are not tzinfos
args = [tuple(), list(), 1, '2011-01-01',
date(2011, 1, 1), datetime(2011, 1, 1)]
for arg in args:
for exact in (True, False):
with self.subTest(arg=arg, exact=exact):
self.assertFalse(is_tzinfo(arg, exact))
def load_tests(loader, standard_tests, pattern): def load_tests(loader, standard_tests, pattern):
standard_tests.addTest(ZoneInfoCompleteTest()) standard_tests.addTest(ZoneInfoCompleteTest())
return standard_tests return standard_tests

View File

@ -0,0 +1,2 @@
Add C API access to the ``datetime.timezone`` constructor and
``datetime.timzone.UTC`` singleton.

View File

@ -6036,10 +6036,12 @@ static PyDateTime_CAPI CAPI = {
&PyDateTime_TimeType, &PyDateTime_TimeType,
&PyDateTime_DeltaType, &PyDateTime_DeltaType,
&PyDateTime_TZInfoType, &PyDateTime_TZInfoType,
NULL, // PyDatetime_TimeZone_UTC not initialized yet
new_date_ex, new_date_ex,
new_datetime_ex, new_datetime_ex,
new_time_ex, new_time_ex,
new_delta_ex, new_delta_ex,
new_timezone,
datetime_fromtimestamp, datetime_fromtimestamp,
date_fromtimestamp, date_fromtimestamp,
new_datetime_ex2, new_datetime_ex2,
@ -6168,6 +6170,7 @@ PyInit__datetime(void)
if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0) if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0)
return NULL; return NULL;
PyDateTime_TimeZone_UTC = x; PyDateTime_TimeZone_UTC = x;
CAPI.TimeZone_UTC = PyDateTime_TimeZone_UTC;
delta = new_delta(-1, 60, 0, 1); /* -23:59 */ delta = new_delta(-1, 60, 0, 1); /* -23:59 */
if (delta == NULL) if (delta == NULL)

View File

@ -2220,12 +2220,92 @@ test_datetime_capi(PyObject *self, PyObject *args) {
} }
test_run_counter++; test_run_counter++;
PyDateTime_IMPORT; PyDateTime_IMPORT;
if (PyDateTimeAPI) if (PyDateTimeAPI)
Py_RETURN_NONE; Py_RETURN_NONE;
else else
return NULL; return NULL;
} }
/* Functions exposing the C API type checking for testing */
#define MAKE_DATETIME_CHECK_FUNC(check_method, exact_method) \
PyObject *obj; \
int exact = 0; \
if (!PyArg_ParseTuple(args, "O|p", &obj, &exact)) { \
return NULL; \
} \
int rv = exact?exact_method(obj):check_method(obj); \
if (rv) { \
Py_RETURN_TRUE; \
} else { \
Py_RETURN_FALSE; \
}
static PyObject *
datetime_check_date(PyObject *self, PyObject *args) {
MAKE_DATETIME_CHECK_FUNC(PyDate_Check, PyDate_CheckExact)
}
static PyObject *
datetime_check_time(PyObject *self, PyObject *args) {
MAKE_DATETIME_CHECK_FUNC(PyTime_Check, PyTime_CheckExact)
}
static PyObject *
datetime_check_datetime(PyObject *self, PyObject *args) {
MAKE_DATETIME_CHECK_FUNC(PyDateTime_Check, PyDateTime_CheckExact)
}
static PyObject *
datetime_check_delta(PyObject *self, PyObject *args) {
MAKE_DATETIME_CHECK_FUNC(PyDelta_Check, PyDelta_CheckExact)
}
static PyObject *
datetime_check_tzinfo(PyObject *self, PyObject *args) {
MAKE_DATETIME_CHECK_FUNC(PyTZInfo_Check, PyTZInfo_CheckExact)
}
/* Makes three variations on timezone representing UTC-5:
1. timezone with offset and name from PyDateTimeAPI
2. timezone with offset and name from PyTimeZone_FromOffsetAndName
3. timezone with offset (no name) from PyTimeZone_FromOffset
*/
static PyObject *
make_timezones_capi(PyObject *self, PyObject *args) {
PyObject *offset = PyDelta_FromDSU(0, -18000, 0);
PyObject *name = PyUnicode_FromString("EST");
PyObject *est_zone_capi = PyDateTimeAPI->TimeZone_FromTimeZone(offset, name);
PyObject *est_zone_macro = PyTimeZone_FromOffsetAndName(offset, name);
PyObject *est_zone_macro_noname = PyTimeZone_FromOffset(offset);
Py_DecRef(offset);
Py_DecRef(name);
PyObject *rv = PyTuple_New(3);
PyTuple_SET_ITEM(rv, 0, est_zone_capi);
PyTuple_SET_ITEM(rv, 1, est_zone_macro);
PyTuple_SET_ITEM(rv, 2, est_zone_macro_noname);
return rv;
}
static PyObject *
get_timezone_utc_capi(PyObject* self, PyObject *args) {
int macro = 0;
if (!PyArg_ParseTuple(args, "|p", &macro)) {
return NULL;
}
if (macro) {
return PyDateTime_TimeZone_UTC;
} else {
return PyDateTimeAPI->TimeZone_UTC;
}
}
/* test_thread_state spawns a thread of its own, and that thread releases /* test_thread_state spawns a thread of its own, and that thread releases
* `thread_done` when it's finished. The driver code has to know when the * `thread_done` when it's finished. The driver code has to know when the
@ -4452,6 +4532,13 @@ static PyMethodDef TestMethods[] = {
{"test_config", (PyCFunction)test_config, METH_NOARGS}, {"test_config", (PyCFunction)test_config, METH_NOARGS},
{"test_sizeof_c_types", (PyCFunction)test_sizeof_c_types, METH_NOARGS}, {"test_sizeof_c_types", (PyCFunction)test_sizeof_c_types, METH_NOARGS},
{"test_datetime_capi", test_datetime_capi, METH_NOARGS}, {"test_datetime_capi", test_datetime_capi, METH_NOARGS},
{"datetime_check_date", datetime_check_date, METH_VARARGS},
{"datetime_check_time", datetime_check_time, METH_VARARGS},
{"datetime_check_datetime", datetime_check_datetime, METH_VARARGS},
{"datetime_check_delta", datetime_check_delta, METH_VARARGS},
{"datetime_check_tzinfo", datetime_check_tzinfo, METH_VARARGS},
{"make_timezones_capi", make_timezones_capi, METH_NOARGS},
{"get_timezone_utc_capi", get_timezone_utc_capi, METH_VARARGS},
{"test_list_api", (PyCFunction)test_list_api, METH_NOARGS}, {"test_list_api", (PyCFunction)test_list_api, METH_NOARGS},
{"test_dict_iteration", (PyCFunction)test_dict_iteration,METH_NOARGS}, {"test_dict_iteration", (PyCFunction)test_dict_iteration,METH_NOARGS},
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS}, {"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},