-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
bpo-15873: add '.fromisoformat' for date, time and datetime #4841
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
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 |
---|---|---|
|
@@ -428,6 +428,19 @@ Other constructors, all class methods: | |
:exc:`ValueError` on :c:func:`localtime` failure. | ||
|
||
|
||
.. classmethod:: date.fromisoformat(date_string) | ||
|
||
Return a date object corresponding to *date_string*, according to RFC 3339, | ||
a stricter, simpler subset of ISO 8601, and such as is returned by | ||
:func:`date.isoformat`. Microseconds are rounded to 6 digits. | ||
:exc:`ValueError` is raised if *date_string* is not a valid RFC 3339 | ||
date string. | ||
|
||
.. `RFC 3339`: https://www.ietf.org/rfc/rfc3339.txt | ||
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. How does this get marked up? I tend to prefer the IETF’s HTML versions (e.g. https://tools.ietf.org/html/rfc3339), and a lot of the documentation seems to link to those directly via |
||
|
||
.. versionadded:: 3.7 | ||
|
||
|
||
.. classmethod:: date.fromordinal(ordinal) | ||
|
||
Return the date corresponding to the proleptic Gregorian ordinal, where January | ||
|
@@ -793,6 +806,19 @@ Other constructors, all class methods: | |
:exc:`ValueError` on :c:func:`gmtime` failure. | ||
|
||
|
||
.. classmethod:: datetime.fromisoformat(datetime_string) | ||
|
||
Return a datetime object corresponding to *datetime_string*, according to RFC 3339, | ||
a stricter, simpler subset of ISO 8601, and such as is returned by | ||
:func:`datetime.isoformat`. Microseconds are rounded to 6 digits. | ||
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. Perhaps it would be clearer to say something like “The time is rounded to a whole number of microseconds”? Also, I suggest clarifying that the separator may be a space instead of T. This is suggested, but not required by the RFC profile, and means that the output of format(datetime) is supported. |
||
:exc:`ValueError` is raised if *datetime_string* is not a valid RFC 3339 | ||
datetime string. | ||
|
||
.. `RFC 3339`: https://www.ietf.org/rfc/rfc3339.txt | ||
|
||
.. versionadded:: 3.7 | ||
|
||
|
||
.. classmethod:: datetime.fromordinal(ordinal) | ||
|
||
Return the :class:`.datetime` corresponding to the proleptic Gregorian ordinal, | ||
|
@@ -1394,6 +1420,22 @@ day, and subject to adjustment via a :class:`tzinfo` object. | |
If an argument outside those ranges is given, :exc:`ValueError` is raised. All | ||
default to ``0`` except *tzinfo*, which defaults to :const:`None`. | ||
|
||
|
||
Other constructor: | ||
|
||
.. classmethod:: time.fromisoformat(string) | ||
|
||
Return a time object corresponding to *time_string*, according to RFC 3339, | ||
a stricter, simpler subset of ISO 8601, and such as is returned by | ||
:func:`time.isoformat`. Microseconds are rounded to 6 digits. | ||
:exc:`ValueError` is raised if *time_string* is not a valid RFC 3339 | ||
time string.. | ||
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. Doubled full stop. |
||
|
||
.. `RFC 3339`: https://www.ietf.org/rfc/rfc3339.txt | ||
|
||
.. versionadded:: 3.7 | ||
|
||
|
||
Class attributes: | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
import locale | ||
import calendar | ||
from re import compile as re_compile | ||
from re import IGNORECASE | ||
from re import IGNORECASE, ASCII | ||
from re import escape as re_escape | ||
from datetime import (date as datetime_date, | ||
timedelta as datetime_timedelta, | ||
|
@@ -27,6 +27,55 @@ def _getlang(): | |
# Figure out what the current language is set to. | ||
return locale.getlocale(locale.LC_TIME) | ||
|
||
|
||
_date_re = re_compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})$', | ||
ASCII) | ||
|
||
_time_re = re_compile(r'(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})' | ||
r'(?P<microsecond>\.\d+)?(?P<tzinfo>Z|[+-]\d{2}:\d{2})?$', | ||
ASCII|IGNORECASE) | ||
|
||
_datetime_re = re_compile(_date_re.pattern[:-1] + r'[T ]' + _time_re.pattern, | ||
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. Maybe, it be easier to define tmp variables to avoid dealing the indexing here:
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'll note that this won't match all the outputs of That said, it seems like date_str = dt_str[0:10]
time_str = dt_str[11:]
dt_sep = dt_str[10:11] It's then kinda trivial to enforce whatever restrictions you want on |
||
ASCII|IGNORECASE) | ||
|
||
|
||
def _parse_isodate(cls, isostring): | ||
return _parse_isoformat(cls, isostring, _date_re) | ||
|
||
|
||
def _parse_isotime(cls, isostring): | ||
return _parse_isoformat(cls, isostring, _time_re) | ||
|
||
|
||
def _parse_isodatetime(cls, isostring): | ||
return _parse_isoformat(cls, isostring, _datetime_re) | ||
|
||
|
||
def _parse_isoformat(cls, isostring, iso_re): | ||
match = iso_re.match(isostring) | ||
if not match: | ||
raise ValueError("invalid RFC 3339 %s string: %r" | ||
% (cls.__name__, isostring)) | ||
kw = match.groupdict() | ||
tzinfo = kw.pop('tzinfo', None) | ||
if tzinfo == 'Z' or tzinfo == 'z': | ||
tzinfo = datetime_timezone.utc | ||
elif tzinfo is not None: | ||
offset_hours, _, offset_mins = tzinfo[1:].partition(':') | ||
offset = datetime_timedelta(hours=int(offset_hours), minutes=int(offset_mins)) | ||
if tzinfo[0] == '-': | ||
offset = -offset | ||
tzinfo = datetime_timezone(offset) | ||
us = kw.pop('microsecond', None) | ||
kw = {k: int(v) for k, v in kw.items()} | ||
if us: | ||
us = round(float(us), 6) | ||
kw['microsecond'] = int(us * 1e6) | ||
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. Both the time and datetime classes require microsecond to be strictly less than 1 million, and it looks like you don’t handle rolling over seconds, minutes, etc. Test case: datetime.fromisoformat('2017-12-31 23:59:59.9999995') -> datetime(2018, 1, 1, 0, 0, 0) For datetime, I might do the rolling over by adding a timedelta. For time, maybe it is not worth doing any rounding. Also, float is approximate, e.g. for me float(".000_001_499_999_999_999_999_99") rounds up over 1.5 µs, which round would round up to 2 µs. I wonder if it is worth doing the rounding without float; then you could claim in the documentation that rounding is always half-to-even. Untested code: _time_re = ... r'(?:(?P<microsecond>\.\d{1,6})(?P<us_frac>\d*))?' ...
...
us = int(float(us) * 1e6)
frac = kw.pop('us_frac').rstrip("0")
# Round halfway up to even rather than down to odd
us += frac > "5" or frac == "5" and us % 2 |
||
if tzinfo: | ||
kw['tzinfo'] = tzinfo | ||
return cls(**kw) | ||
|
||
|
||
class LocaleTime(object): | ||
"""Stores and handles locale-specific information related to time. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -732,6 +732,15 @@ def fromordinal(cls, n): | |
y, m, d = _ord2ymd(n) | ||
return cls(y, m, d) | ||
|
||
@classmethod | ||
def fromisoformat(cls, date_string): | ||
"""Constructs a date from an RFC 3339 string, a strict subset of ISO 8601. | ||
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. Convention is to use imperative form ("Construct"). This comment applies in other places. |
||
|
||
Raises ValueError in case of ill-formatted or invalid string. | ||
""" | ||
import _strptime | ||
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. Any reason not to have this at the top level ? |
||
return _strptime._parse_isodate(cls, date_string) | ||
|
||
# Conversions to string | ||
|
||
def __repr__(self): | ||
|
@@ -1075,6 +1084,16 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold | |
self._fold = fold | ||
return self | ||
|
||
@classmethod | ||
def fromisoformat(cls, time_string): | ||
"""Constructs a time from an RFC 3339 string, a strict subset of ISO 8601. | ||
Microseconds are rounded to 6 digits. | ||
|
||
Raises ValueError in case of ill-formatted or invalid string. | ||
""" | ||
import _strptime | ||
return _strptime._parse_isotime(cls, time_string) | ||
|
||
# Read-only field accessors | ||
@property | ||
def hour(self): | ||
|
@@ -1472,6 +1491,16 @@ def utcfromtimestamp(cls, t): | |
"""Construct a naive UTC datetime from a POSIX timestamp.""" | ||
return cls._fromtimestamp(t, True, None) | ||
|
||
@classmethod | ||
def fromisoformat(cls, datetime_string): | ||
"""Constructs a datetime from an RFC 3339 string, a strict subset of ISO 8601. | ||
Microseconds are rounded to 6 digits. | ||
|
||
Raises ValueError in case of ill-formatted or invalid string. | ||
""" | ||
import _strptime | ||
return _strptime._parse_isodatetime(cls, datetime_string) | ||
|
||
@classmethod | ||
def now(cls, tz=None): | ||
"Construct a datetime from time.time() and optional time zone info." | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2607,6 +2607,25 @@ date_fromtimestamp(PyObject *cls, PyObject *args) | |
return result; | ||
} | ||
|
||
/* Return new date from given date string, using _strptime._parse_isodate(). */ | ||
static PyObject * | ||
date_fromisoformat(PyObject *cls, PyObject *args) | ||
{ | ||
static PyObject *module = NULL; | ||
PyObject *string; | ||
|
||
if (!PyArg_ParseTuple(args, "U:fromisoformat", &string)) | ||
return NULL; | ||
|
||
if (module == NULL) { | ||
module = PyImport_ImportModule("_strptime"); | ||
if (module == NULL) | ||
return NULL; | ||
} | ||
|
||
return PyObject_CallMethod(module, "_parse_isodate", "OO", cls, string); | ||
} | ||
|
||
/* Return new date from proleptic Gregorian ordinal. Raises ValueError if | ||
* the ordinal is out of range. | ||
*/ | ||
|
@@ -2920,6 +2939,11 @@ static PyMethodDef date_methods[] = { | |
PyDoc_STR("timestamp -> local date from a POSIX timestamp (like " | ||
"time.time()).")}, | ||
|
||
{"fromisoformat", (PyCFunction)date_fromisoformat, | ||
METH_VARARGS | METH_CLASS, | ||
PyDoc_STR("Construct a date from an RFC 3339 string, a strict subset of ISO 8601.\n" | ||
"Raises ValueError in case of ill-formatted or invalid string.\n")}, | ||
|
||
{"fromordinal", (PyCFunction)date_fromordinal, METH_VARARGS | | ||
METH_CLASS, | ||
PyDoc_STR("int -> date corresponding to a proleptic Gregorian " | ||
|
@@ -3711,6 +3735,26 @@ time_str(PyDateTime_Time *self) | |
return _PyObject_CallMethodId((PyObject *)self, &PyId_isoformat, NULL); | ||
} | ||
|
||
/* Return new time from time string, using _strptime._parse_isotime(). */ | ||
static PyObject * | ||
time_fromisoformat(PyObject *cls, PyObject *args) | ||
{ | ||
static PyObject *module = NULL; | ||
PyObject *string; | ||
|
||
if (!PyArg_ParseTuple(args, "U:fromisoformat", &string)) | ||
return NULL; | ||
|
||
|
||
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. You've used only one blank line in the other places (which is IMHO more beautiful). Also, do you reckon it would make sense to have a single function like:
|
||
if (module == NULL) { | ||
module = PyImport_ImportModule("_strptime"); | ||
if (module == NULL) | ||
return NULL; | ||
} | ||
|
||
return PyObject_CallMethod(module, "_parse_isotime", "OO", cls, string); | ||
} | ||
|
||
static PyObject * | ||
time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw) | ||
{ | ||
|
@@ -4018,6 +4062,13 @@ time_reduce(PyDateTime_Time *self, PyObject *arg) | |
|
||
static PyMethodDef time_methods[] = { | ||
|
||
{"fromisoformat", (PyCFunction)time_fromisoformat, | ||
METH_VARARGS | METH_CLASS, | ||
PyDoc_STR("Construct a time from an RFC 3339 string, a strict subset " | ||
"of ISO 8601.\n" | ||
"Microseconds are rounded to 6 digits.\n" | ||
"Raises ValueError in case of ill-formatted or invalid string.\n")}, | ||
|
||
{"isoformat", (PyCFunction)time_isoformat, METH_VARARGS | METH_KEYWORDS, | ||
PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]" | ||
"[+HH:MM].\n\n" | ||
|
@@ -4726,6 +4777,25 @@ datetime_str(PyDateTime_DateTime *self) | |
return _PyObject_CallMethodId((PyObject *)self, &PyId_isoformat, "s", " "); | ||
} | ||
|
||
/* Return new datetime from _strptime._parse_isodatetime(). */ | ||
static PyObject * | ||
datetime_fromisoformat(PyObject *cls, PyObject *args) | ||
{ | ||
static PyObject *module = NULL; | ||
PyObject *string; | ||
|
||
if (!PyArg_ParseTuple(args, "U:fromisoformat", &string)) | ||
return NULL; | ||
|
||
if (module == NULL) { | ||
module = PyImport_ImportModule("_strptime"); | ||
if (module == NULL) | ||
return NULL; | ||
} | ||
|
||
return PyObject_CallMethod(module, "_parse_isodatetime", "OO", cls, string); | ||
} | ||
|
||
static PyObject * | ||
datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) | ||
{ | ||
|
@@ -5506,6 +5576,12 @@ static PyMethodDef datetime_methods[] = { | |
METH_VARARGS | METH_KEYWORDS | METH_CLASS, | ||
PyDoc_STR("timestamp[, tz] -> tz's local time from POSIX timestamp.")}, | ||
|
||
{"fromisoformat", (PyCFunction)datetime_fromisoformat, | ||
METH_VARARGS | METH_CLASS, | ||
PyDoc_STR("Construct a datetime from an RFC 3339 string, a strict subset of ISO 8601.\n" | ||
"Microseconds are rounded to 6 digits.\n" | ||
"Raises ValueError in case of ill-formatted or invalid string.\n")}, | ||
|
||
{"utcfromtimestamp", (PyCFunction)datetime_utcfromtimestamp, | ||
METH_VARARGS | METH_CLASS, | ||
PyDoc_STR("Construct a naive UTC datetime from a POSIX timestamp.")}, | ||
|
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.
Shouldn’t microseconds in a date string be illegal? Maybe clarify which parts of the RFC are relevant; perhaps the full-date format?