-
-
Notifications
You must be signed in to change notification settings - Fork 32k
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
Changes from all commits
7f1878b
20676ae
4a1bab6
8728bd7
2c20871
0398e3c
70642af
9303050
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reference counting information is stored in Doc/data/refcounts.dat There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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()`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -155,12 +155,16 @@ typedef struct { | |
PyTypeObject *DeltaType; | ||
PyTypeObject *TZInfoType; | ||
|
||
/* singletons */ | ||
PyObject *TimeZone_UTC; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
The idea is to make code in user extensions look similar to that in _datetimemodule.c. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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*); | ||
|
@@ -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( \ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,8 @@ | |
from datetime import date, datetime | ||
import time as _time | ||
|
||
import _testcapi | ||
|
||
# Needed by test_datetime | ||
import _strptime | ||
# | ||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, good call, as written this test doesn't actually check if |
||
|
||
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 | ||
|
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. |
There was a problem hiding this comment.
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?