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
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:
.. 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)
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)
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.
.. 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.
.. c:function:: PyObject* PyDelta_FromDSU(int days, int seconds, int useconds)
Return a ``datetime.timedelta`` object representing the given number of days,
seconds and microseconds. Normalization is performed so that the resulting
number of microseconds and seconds lie in the ranges documented for
``datetime.timedelta`` objects.
Return a :class:`datetime.timedelta` object representing the given number
of days, seconds and microseconds. Normalization is performed so that the
resulting number of microseconds and seconds lie in the ranges documented for
: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
@ -199,11 +223,11 @@ Macros for the convenience of modules implementing the DB API:
.. c:function:: PyObject* PyDateTime_FromTimestamp(PyObject *args)
Create and return a new ``datetime.datetime`` object given an argument tuple
suitable for passing to ``datetime.datetime.fromtimestamp()``.
Create and return a new :class:`datetime.datetime` object given an argument
tuple suitable for passing to :meth:`datetime.datetime.fromtimestamp()`.
.. c:function:: PyObject* PyDate_FromTimestamp(PyObject *args)
Create and return a new ``datetime.date`` object given an argument tuple
suitable for passing to ``datetime.date.fromtimestamp()``.
Create and return a new :class:`datetime.date` object given an argument
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: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:PyTypeObject*:type::
PyDescr_NewClassMethod:PyMethodDef*:method::

View File

@ -155,12 +155,16 @@ typedef struct {
PyTypeObject *DeltaType;
PyTypeObject *TZInfoType;
/* singletons */
PyObject *TimeZone_UTC;
/* constructors */
PyObject *(*Date_FromDate)(int, int, int, PyTypeObject*);
PyObject *(*DateTime_FromDateAndTime)(int, int, int, int, int, int, int,
PyObject*, PyTypeObject*);
PyObject *(*Time_FromTime)(int, int, int, int, PyObject*, PyTypeObject*);
PyObject *(*Delta_FromDelta)(int, int, int, int, PyTypeObject*);
PyObject *(*TimeZone_FromTimeZone)(PyObject *offset, PyObject *name);
/* constructors for the DB API */
PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*);
@ -202,6 +206,9 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
#define PyDateTime_IMPORT \
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. */
#define PyDate_Check(op) PyObject_TypeCheck(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->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. */
#define PyDateTime_FromTimestamp(args) \
PyDateTimeAPI->DateTime_FromTimestamp( \

View File

@ -31,6 +31,8 @@ from datetime import timezone
from datetime import date, datetime
import time as _time
import _testcapi
# Needed by test_datetime
import _strptime
#
@ -5443,6 +5445,185 @@ class ZoneInfoCompleteTest(unittest.TestSuite):
class IranTest(ZoneInfoTest):
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):
standard_tests.addTest(ZoneInfoCompleteTest())
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_DeltaType,
&PyDateTime_TZInfoType,
NULL, // PyDatetime_TimeZone_UTC not initialized yet
new_date_ex,
new_datetime_ex,
new_time_ex,
new_delta_ex,
new_timezone,
datetime_fromtimestamp,
date_fromtimestamp,
new_datetime_ex2,
@ -6168,6 +6170,7 @@ PyInit__datetime(void)
if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0)
return NULL;
PyDateTime_TimeZone_UTC = x;
CAPI.TimeZone_UTC = PyDateTime_TimeZone_UTC;
delta = new_delta(-1, 60, 0, 1); /* -23:59 */
if (delta == NULL)

View File

@ -2220,12 +2220,92 @@ test_datetime_capi(PyObject *self, PyObject *args) {
}
test_run_counter++;
PyDateTime_IMPORT;
if (PyDateTimeAPI)
Py_RETURN_NONE;
else
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
* `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_sizeof_c_types", (PyCFunction)test_sizeof_c_types, 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_dict_iteration", (PyCFunction)test_dict_iteration,METH_NOARGS},
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},