diff --git a/Doc/c-api/datetime.rst b/Doc/c-api/datetime.rst index 305e990368c..78724619ea3 100644 --- a/Doc/c-api/datetime.rst +++ b/Doc/c-api/datetime.rst @@ -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()`. diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index b1cad48c3e5..6dc86fc5e54 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -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:: diff --git a/Include/datetime.h b/Include/datetime.h index 3bf35cbb7f2..059d5ecf7a2 100644 --- a/Include/datetime.h +++ b/Include/datetime.h @@ -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( \ diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e8ed79e8b32..a0883b9f421 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -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 diff --git a/Misc/NEWS.d/next/C API/2017-12-28-15-22-05.bpo-10381.a1E6aF.rst b/Misc/NEWS.d/next/C API/2017-12-28-15-22-05.bpo-10381.a1E6aF.rst new file mode 100644 index 00000000000..26717692d03 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2017-12-28-15-22-05.bpo-10381.a1E6aF.rst @@ -0,0 +1,2 @@ +Add C API access to the ``datetime.timezone`` constructor and +``datetime.timzone.UTC`` singleton. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index d1f48e5bd04..4a33f2d8964 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -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) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e3be7d3d829..3f41134a345 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -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},