Skip to content

bpo-10381: Add timezone to datetime C API #5032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions Doc/c-api/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abalkin I had to add a new section for this and decided to put it at the top, please let me know if you'd prefer it to be somewhere else.

@vadmium You did a great review of the documentation on #4699, do you want to take a look at this?


.. 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)
Expand Down Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I build the documentation, I see Return value: New reference. under all the constructor macros except these two, but I'm not sure where that's being generated from. I don't see why they wouldn't be a new reference, but I don't 100% grok when something does and does not change the reference count.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference counting information is stored in Doc/data/refcounts.dat

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abalkin When documenting the reference counts, I ran into a bit of a problem, since the reference to offset is not increased if offset is equivalent to timedelta(0), so it's ambiguous whether the function's effect on offset's reference count is +1 or 0. I went with +1 and added a comment to the effect that it's not always true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to merge this now. We can fine tune the documentation during the beta phase.


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
Expand Down Expand Up @@ -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()`.
8 changes: 8 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
13 changes: 13 additions & 0 deletions Include/datetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,16 @@ typedef struct {
PyTypeObject *DeltaType;
PyTypeObject *TZInfoType;

/* singletons */
PyObject *TimeZone_UTC;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also add a macro definition below to simplify access to the singleton?

#ifdef Py_BUILD_CORE
 ..
#else
#define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC
#endif

The idea is to make code in user extensions look similar to that in _datetimemodule.c.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm fine with that. I think I didn't know of any examples of other singletons exposed in the C API so I didn't know to do this.


/* 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*);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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( \
Expand Down
181 changes: 181 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from datetime import date, datetime
import time as _time

import _testcapi

# Needed by test_datetime
import _strptime
#
Expand Down Expand Up @@ -5443,6 +5445,185 @@ def __init__(self):
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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there is no practical difference between these two as far as the test is concerned, so I suggest to drop one of them. Comparison of datetime objects only considers the offset.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, good call, as written this test doesn't actually check if PyTimeZone_FromOffsetAndName actually works.


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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add C API access to the ``datetime.timezone`` constructor and
``datetime.timzone.UTC`` singleton.
3 changes: 3 additions & 0 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading