-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
bpo-15873: Implement [date][time].fromisoformat #4699
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 18 commits
b451a83
4ccef0a
13e8e48
6fa5c55
0d44220
327d0fc
52b2175
f1b78af
aeaa9ca
094ccf4
2a8120d
af9e6d0
cf802af
626d239
7c771e7
8fbd752
4d55e05
5a233fb
9fa91db
ffdb2af
18a5fa8
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 |
|---|---|---|
|
|
@@ -436,6 +436,21 @@ Other constructors, all class methods: | |
| d``. | ||
|
|
||
|
|
||
| .. classmethod:: date.fromisoformat(date_string) | ||
|
|
||
| Return a :class:`date` corresponding to a *date_string* in one of the ISO 8601 | ||
| formats emitted by :meth:`date.isoformat`. Specifically, this function supports | ||
| strings in the format(s) ``YYYY-MM-DD``. | ||
|
|
||
| .. caution:: | ||
|
|
||
| This does not support parsing arbitrary ISO 8601 strings - it is only intended | ||
| as the inverse operation of :meth:`date.isoformat`. | ||
|
|
||
| .. versionadded:: 3.7 | ||
|
|
||
|
|
||
|
|
||
| Class attributes: | ||
|
|
||
| .. attribute:: date.min | ||
|
|
@@ -819,6 +834,20 @@ Other constructors, all class methods: | |
| Added the *tzinfo* argument. | ||
|
|
||
|
|
||
| .. classmethod:: datetime.fromisoformat(date_string) | ||
|
|
||
| Return a :class:`datetime` corresponding to a *date_string* in one of the | ||
| ISO 8601 formats emitted by :meth:`datetime.isoformat`. Specifically, this function | ||
|
||
| supports strings in the format(s) ``YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]]][+HH:MM[:SS[.ffffff]]]``, | ||
| where ``*`` can match any single character. | ||
|
||
|
|
||
| .. caution:: | ||
|
|
||
| This does not support parsing arbitrary ISO 8601 strings - it is only intended | ||
| as the inverse operation of :meth:`datetime.isoformat`. | ||
|
|
||
| .. versionadded:: 3.7 | ||
|
|
||
| .. classmethod:: datetime.strptime(date_string, format) | ||
|
|
||
| Return a :class:`.datetime` corresponding to *date_string*, parsed according to | ||
|
|
@@ -1486,6 +1515,23 @@ In boolean contexts, a :class:`.time` object is always considered to be true. | |
| error-prone and has been removed in Python 3.5. See :issue:`13936` for full | ||
| details. | ||
|
|
||
|
|
||
| Other constructor: | ||
|
|
||
| .. classmethod:: time.fromisoformat(date_string) | ||
|
||
|
|
||
| Return a :class:`time` corresponding to a *time_string* in one of the ISO 8601 | ||
| formats emitted by :meth:`time.isoformat`. Specifically, this function supports | ||
| strings in the format(s) ``HH[:MM[:SS[.mmm[mmm]]]]][+HH:MM[:SS[.ffffff]]]``. | ||
|
||
|
|
||
| .. caution:: | ||
|
|
||
| This does not support parsing arbitrary ISO 8601 strings - it is only intended | ||
| as the inverse operation of :meth:`time.isoformat`. | ||
|
|
||
| .. versionadded:: 3.7 | ||
|
|
||
|
|
||
| Instance methods: | ||
|
|
||
| .. method:: time.replace(hour=self.hour, minute=self.minute, second=self.second, \ | ||
|
|
@@ -1587,7 +1633,6 @@ Instance methods: | |
| ``self.tzinfo.tzname(None)``, or raises an exception if the latter doesn't | ||
| return ``None`` or a string object. | ||
|
|
||
|
|
||
| Example: | ||
|
|
||
| >>> from datetime import time, tzinfo, timedelta | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -173,6 +173,24 @@ def _format_time(hh, mm, ss, us, timespec='auto'): | |
| else: | ||
| return fmt.format(hh, mm, ss, us) | ||
|
|
||
| def _format_offset(off): | ||
| s = '' | ||
| if off is not None: | ||
| if off.days < 0: | ||
| sign = "-" | ||
| off = -off | ||
| else: | ||
| sign = "+" | ||
| hh, mm = divmod(off, timedelta(hours=1)) | ||
| mm, ss = divmod(mm, timedelta(minutes=1)) | ||
| s += "%s%02d:%02d" % (sign, hh, mm) | ||
| if ss or ss.microseconds: | ||
| s += ":%02d" % ss.seconds | ||
|
|
||
| if ss.microseconds: | ||
| s += '.%06d' % ss.microseconds | ||
| return s | ||
|
|
||
| # Correctly substitute for %z and %Z escapes in strftime formats. | ||
| def _wrap_strftime(object, format, timetuple): | ||
| # Don't call utcoffset() or tzname() unless actually needed. | ||
|
|
@@ -237,6 +255,102 @@ def _wrap_strftime(object, format, timetuple): | |
| newformat = "".join(newformat) | ||
| return _time.strftime(newformat, timetuple) | ||
|
|
||
| # Helpers for parsing the result of isoformat() | ||
| def _parse_isoformat_date(dtstr): | ||
| # It is assumed that this function will only be called with a | ||
| # string of length exactly 10, and (though this is not used) ASCII-only | ||
| year = int(dtstr[0:4]) | ||
| if dtstr[4] != '-': | ||
| raise ValueError('Invalid date separator: %s' % dtstr[4]) | ||
|
|
||
| month = int(dtstr[5:7]) | ||
|
|
||
| if dtstr[7] != '-': | ||
| raise ValueError('Invalid date separator') | ||
|
|
||
| day = int(dtstr[8:10]) | ||
|
|
||
| return [year, month, day] | ||
|
|
||
| def _parse_hh_mm_ss_ff(tstr): | ||
| # Parses things of the form HH[:MM[:SS[.fff[fff]]]] | ||
| len_str = len(tstr) | ||
|
|
||
| time_comps = [0, 0, 0, 0] | ||
| pos = 0 | ||
| for comp in range(0, 3): | ||
| if (len_str - pos) < 2: | ||
| raise ValueError('Incomplete time component') | ||
|
|
||
| time_comps[comp] = int(tstr[pos:pos+2]) | ||
|
|
||
| pos += 2 | ||
| next_char = tstr[pos:pos+1] | ||
|
|
||
| if not next_char or comp >= 2: | ||
| break | ||
|
|
||
| if next_char != ':': | ||
| raise ValueError('Invalid time separator: %c' % next_char) | ||
|
|
||
| pos += 1 | ||
|
|
||
| if pos < len_str: | ||
| if tstr[pos] != '.': | ||
| raise ValueError('Invalid microsecond component') | ||
| else: | ||
| pos += 1 | ||
|
|
||
| len_remainder = len_str - pos | ||
| if len_remainder not in (3, 6): | ||
| raise ValueError('Invalid microsecond component') | ||
|
|
||
| time_comps[3] = int(tstr[pos:]) | ||
| if len_remainder == 3: | ||
| time_comps[3] *= 1000 | ||
|
|
||
| return time_comps | ||
|
|
||
| def _parse_isoformat_time(tstr): | ||
| # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] | ||
| len_str = len(tstr) | ||
| if len_str < 2: | ||
| raise ValueError('Isoformat time too short') | ||
|
|
||
| # This is equivalent to re.search('[+-]', tstr), but faster | ||
| tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1) | ||
| timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr | ||
|
|
||
| time_comps = _parse_hh_mm_ss_ff(timestr) | ||
|
|
||
| tzi = None | ||
| if tz_pos > 0: | ||
| tzstr = tstr[tz_pos:] | ||
|
|
||
| # Valid time zone strings are: | ||
| # HH:MM len: 5 | ||
| # HH:MM:SS len: 8 | ||
| # HH:MM:SS.ffffff len: 15 | ||
|
|
||
| if len(tzstr) not in (5, 8, 15): | ||
| raise ValueError('Malformed time zone string') | ||
|
|
||
| tz_comps = _parse_hh_mm_ss_ff(tzstr) | ||
| if all(x == 0 for x in tz_comps): | ||
| tzi = timezone.utc | ||
| else: | ||
| tzsign = -1 if tstr[tz_pos - 1] == '-' else 1 | ||
|
|
||
| td = timedelta(hours=tz_comps[0], minutes=tz_comps[1], | ||
| seconds=tz_comps[2], microseconds=tz_comps[3]) | ||
|
|
||
| tzi = timezone(tzsign * td) | ||
|
|
||
| time_comps.append(tzi) | ||
|
|
||
| return time_comps | ||
|
|
||
|
|
||
| # Just raise TypeError if the arg isn't None or a string. | ||
| def _check_tzname(name): | ||
| if name is not None and not isinstance(name, str): | ||
|
|
@@ -732,6 +846,19 @@ def fromordinal(cls, n): | |
| y, m, d = _ord2ymd(n) | ||
| return cls(y, m, d) | ||
|
|
||
| @classmethod | ||
| def fromisoformat(cls, date_string): | ||
| """Construct a date from the output of date.isoformat().""" | ||
| if not isinstance(date_string, str): | ||
| raise TypeError('fromisoformat: argument must be str') | ||
|
|
||
| try: | ||
| assert len(date_string) == 10 | ||
| return cls(*_parse_isoformat_date(date_string)) | ||
| except: | ||
|
||
| raise ValueError('Invalid isoformat string: %s' % date_string) | ||
|
|
||
|
|
||
| # Conversions to string | ||
|
|
||
| def __repr__(self): | ||
|
|
@@ -1193,19 +1320,7 @@ def __hash__(self): | |
| def _tzstr(self, sep=":"): | ||
|
||
| """Return formatted timezone offset (+xx:xx) or None.""" | ||
|
||
| off = self.utcoffset() | ||
| if off is not None: | ||
| if off.days < 0: | ||
| sign = "-" | ||
| off = -off | ||
| else: | ||
| sign = "+" | ||
| hh, mm = divmod(off, timedelta(hours=1)) | ||
| mm, ss = divmod(mm, timedelta(minutes=1)) | ||
| assert 0 <= hh < 24 | ||
| off = "%s%02d%s%02d" % (sign, hh, sep, mm) | ||
| if ss: | ||
| off += ':%02d' % ss.seconds | ||
| return off | ||
| return _format_offset(off) | ||
|
|
||
| def __repr__(self): | ||
| """Convert to formal string, for repr().""" | ||
|
|
@@ -1244,6 +1359,18 @@ def isoformat(self, timespec='auto'): | |
|
|
||
| __str__ = isoformat | ||
|
|
||
| @classmethod | ||
| def fromisoformat(cls, time_string): | ||
| """Construct a time from the output of isoformat().""" | ||
| if not isinstance(time_string, str): | ||
| raise TypeError('fromisoformat: argument must be str') | ||
|
||
|
|
||
| try: | ||
| return cls(*_parse_isoformat_time(time_string)) | ||
| except: | ||
| raise ValueError('Invalid isoformat string: %s' % time_string) | ||
|
|
||
|
|
||
| def strftime(self, fmt): | ||
| """Format using strftime(). The date part of the timestamp passed | ||
| to underlying strftime should not be used. | ||
|
|
@@ -1497,6 +1624,31 @@ def combine(cls, date, time, tzinfo=True): | |
| time.hour, time.minute, time.second, time.microsecond, | ||
| tzinfo, fold=time.fold) | ||
|
|
||
| @classmethod | ||
| def fromisoformat(cls, date_string): | ||
| """Construct a datetime from the output of datetime.isoformat().""" | ||
| if not isinstance(date_string, str): | ||
| raise TypeError('fromisoformat: argument must be str') | ||
|
|
||
| # Split this at the separator | ||
| dstr = date_string[0:10] | ||
| tstr = date_string[11:] | ||
|
|
||
| try: | ||
| date_components = _parse_isoformat_date(dstr) | ||
| except ValueError: | ||
| raise ValueError('Invalid isoformat string: %s' % date_string) | ||
|
|
||
| if tstr: | ||
| try: | ||
| time_components = _parse_isoformat_time(tstr) | ||
| except ValueError: | ||
| raise ValueError('Invalid isoformat string: %s' % date_string) | ||
| else: | ||
| time_components = [0, 0, 0, 0, None] | ||
|
|
||
| return cls(*(date_components + time_components)) | ||
|
|
||
| def timetuple(self): | ||
| "Return local time tuple compatible with time.localtime()." | ||
| dst = self.dst() | ||
|
|
@@ -1673,18 +1825,10 @@ def isoformat(self, sep='T', timespec='auto'): | |
| self._microsecond, timespec)) | ||
|
|
||
| off = self.utcoffset() | ||
| if off is not None: | ||
| if off.days < 0: | ||
| sign = "-" | ||
| off = -off | ||
| else: | ||
| sign = "+" | ||
| hh, mm = divmod(off, timedelta(hours=1)) | ||
| mm, ss = divmod(mm, timedelta(minutes=1)) | ||
| s += "%s%02d:%02d" % (sign, hh, mm) | ||
| if ss: | ||
| assert not ss.microseconds | ||
| s += ":%02d" % ss.seconds | ||
| tz = _format_offset(off) | ||
| if tz: | ||
| s += tz | ||
|
|
||
| return s | ||
|
|
||
| def __repr__(self): | ||
|
|
@@ -2275,9 +2419,10 @@ def _name_from_offset(delta): | |
| _check_date_fields, _check_int_field, _check_time_fields, | ||
| _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror, | ||
| _date_class, _days_before_month, _days_before_year, _days_in_month, | ||
| _format_time, _is_leap, _isoweek1monday, _math, _ord2ymd, | ||
| _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, | ||
| _divide_and_round) | ||
| _format_time, _format_offset, _is_leap, _isoweek1monday, _math, | ||
| _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, | ||
| _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, | ||
| _parse_hh_mm_ss_ff) | ||
| # XXX Since import * above excludes names that start with _, | ||
| # docstring does not get overwritten. In the future, it may be | ||
| # appropriate to maintain a single module level docstring and | ||
|
|
||
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.
There is only one format emitted by date.isoformat and supported by your date.fromisoformat, as far as I know.