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:
parent
ccbe5818af
commit
04af5b1ba9
|
@ -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()`.
|
||||
|
|
|
@ -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::
|
||||
|
|
|
@ -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( \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Add C API access to the ``datetime.timezone`` constructor and
|
||||
``datetime.timzone.UTC`` singleton.
|
|
@ -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)
|
||||
|
|
|
@ -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", ¯o)) {
|
||||
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},
|
||||
|
|
Loading…
Reference in New Issue