@@ -173,6 +173,24 @@ def _format_time(hh, mm, ss, us, timespec='auto'):
173173 else :
174174 return fmt .format (hh , mm , ss , us )
175175
176+ def _format_offset (off ):
177+ s = ''
178+ if off is not None :
179+ if off .days < 0 :
180+ sign = "-"
181+ off = - off
182+ else :
183+ sign = "+"
184+ hh , mm = divmod (off , timedelta (hours = 1 ))
185+ mm , ss = divmod (mm , timedelta (minutes = 1 ))
186+ s += "%s%02d:%02d" % (sign , hh , mm )
187+ if ss or ss .microseconds :
188+ s += ":%02d" % ss .seconds
189+
190+ if ss .microseconds :
191+ s += '.%06d' % ss .microseconds
192+ return s
193+
176194# Correctly substitute for %z and %Z escapes in strftime formats.
177195def _wrap_strftime (object , format , timetuple ):
178196 # Don't call utcoffset() or tzname() unless actually needed.
@@ -237,6 +255,102 @@ def _wrap_strftime(object, format, timetuple):
237255 newformat = "" .join (newformat )
238256 return _time .strftime (newformat , timetuple )
239257
258+ # Helpers for parsing the result of isoformat()
259+ def _parse_isoformat_date (dtstr ):
260+ # It is assumed that this function will only be called with a
261+ # string of length exactly 10, and (though this is not used) ASCII-only
262+ year = int (dtstr [0 :4 ])
263+ if dtstr [4 ] != '-' :
264+ raise ValueError ('Invalid date separator: %s' % dtstr [4 ])
265+
266+ month = int (dtstr [5 :7 ])
267+
268+ if dtstr [7 ] != '-' :
269+ raise ValueError ('Invalid date separator' )
270+
271+ day = int (dtstr [8 :10 ])
272+
273+ return [year , month , day ]
274+
275+ def _parse_hh_mm_ss_ff (tstr ):
276+ # Parses things of the form HH[:MM[:SS[.fff[fff]]]]
277+ len_str = len (tstr )
278+
279+ time_comps = [0 , 0 , 0 , 0 ]
280+ pos = 0
281+ for comp in range (0 , 3 ):
282+ if (len_str - pos ) < 2 :
283+ raise ValueError ('Incomplete time component' )
284+
285+ time_comps [comp ] = int (tstr [pos :pos + 2 ])
286+
287+ pos += 2
288+ next_char = tstr [pos :pos + 1 ]
289+
290+ if not next_char or comp >= 2 :
291+ break
292+
293+ if next_char != ':' :
294+ raise ValueError ('Invalid time separator: %c' % next_char )
295+
296+ pos += 1
297+
298+ if pos < len_str :
299+ if tstr [pos ] != '.' :
300+ raise ValueError ('Invalid microsecond component' )
301+ else :
302+ pos += 1
303+
304+ len_remainder = len_str - pos
305+ if len_remainder not in (3 , 6 ):
306+ raise ValueError ('Invalid microsecond component' )
307+
308+ time_comps [3 ] = int (tstr [pos :])
309+ if len_remainder == 3 :
310+ time_comps [3 ] *= 1000
311+
312+ return time_comps
313+
314+ def _parse_isoformat_time (tstr ):
315+ # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
316+ len_str = len (tstr )
317+ if len_str < 2 :
318+ raise ValueError ('Isoformat time too short' )
319+
320+ # This is equivalent to re.search('[+-]', tstr), but faster
321+ tz_pos = (tstr .find ('-' ) + 1 or tstr .find ('+' ) + 1 )
322+ timestr = tstr [:tz_pos - 1 ] if tz_pos > 0 else tstr
323+
324+ time_comps = _parse_hh_mm_ss_ff (timestr )
325+
326+ tzi = None
327+ if tz_pos > 0 :
328+ tzstr = tstr [tz_pos :]
329+
330+ # Valid time zone strings are:
331+ # HH:MM len: 5
332+ # HH:MM:SS len: 8
333+ # HH:MM:SS.ffffff len: 15
334+
335+ if len (tzstr ) not in (5 , 8 , 15 ):
336+ raise ValueError ('Malformed time zone string' )
337+
338+ tz_comps = _parse_hh_mm_ss_ff (tzstr )
339+ if all (x == 0 for x in tz_comps ):
340+ tzi = timezone .utc
341+ else :
342+ tzsign = - 1 if tstr [tz_pos - 1 ] == '-' else 1
343+
344+ td = timedelta (hours = tz_comps [0 ], minutes = tz_comps [1 ],
345+ seconds = tz_comps [2 ], microseconds = tz_comps [3 ])
346+
347+ tzi = timezone (tzsign * td )
348+
349+ time_comps .append (tzi )
350+
351+ return time_comps
352+
353+
240354# Just raise TypeError if the arg isn't None or a string.
241355def _check_tzname (name ):
242356 if name is not None and not isinstance (name , str ):
@@ -732,6 +846,19 @@ def fromordinal(cls, n):
732846 y , m , d = _ord2ymd (n )
733847 return cls (y , m , d )
734848
849+ @classmethod
850+ def fromisoformat (cls , date_string ):
851+ """Construct a date from the output of date.isoformat()."""
852+ if not isinstance (date_string , str ):
853+ raise TypeError ('fromisoformat: argument must be str' )
854+
855+ try :
856+ assert len (date_string ) == 10
857+ return cls (* _parse_isoformat_date (date_string ))
858+ except Exception :
859+ raise ValueError ('Invalid isoformat string: %s' % date_string )
860+
861+
735862 # Conversions to string
736863
737864 def __repr__ (self ):
@@ -1190,22 +1317,10 @@ def __hash__(self):
11901317
11911318 # Conversion to string
11921319
1193- def _tzstr (self , sep = ":" ):
1194- """Return formatted timezone offset (+xx:xx) or None ."""
1320+ def _tzstr (self ):
1321+ """Return formatted timezone offset (+xx:xx) or an empty string ."""
11951322 off = self .utcoffset ()
1196- if off is not None :
1197- if off .days < 0 :
1198- sign = "-"
1199- off = - off
1200- else :
1201- sign = "+"
1202- hh , mm = divmod (off , timedelta (hours = 1 ))
1203- mm , ss = divmod (mm , timedelta (minutes = 1 ))
1204- assert 0 <= hh < 24
1205- off = "%s%02d%s%02d" % (sign , hh , sep , mm )
1206- if ss :
1207- off += ':%02d' % ss .seconds
1208- return off
1323+ return _format_offset (off )
12091324
12101325 def __repr__ (self ):
12111326 """Convert to formal string, for repr()."""
@@ -1244,6 +1359,18 @@ def isoformat(self, timespec='auto'):
12441359
12451360 __str__ = isoformat
12461361
1362+ @classmethod
1363+ def fromisoformat (cls , time_string ):
1364+ """Construct a time from the output of isoformat()."""
1365+ if not isinstance (time_string , str ):
1366+ raise TypeError ('fromisoformat: argument must be str' )
1367+
1368+ try :
1369+ return cls (* _parse_isoformat_time (time_string ))
1370+ except Exception :
1371+ raise ValueError ('Invalid isoformat string: %s' % time_string )
1372+
1373+
12471374 def strftime (self , fmt ):
12481375 """Format using strftime(). The date part of the timestamp passed
12491376 to underlying strftime should not be used.
@@ -1497,6 +1624,31 @@ def combine(cls, date, time, tzinfo=True):
14971624 time .hour , time .minute , time .second , time .microsecond ,
14981625 tzinfo , fold = time .fold )
14991626
1627+ @classmethod
1628+ def fromisoformat (cls , date_string ):
1629+ """Construct a datetime from the output of datetime.isoformat()."""
1630+ if not isinstance (date_string , str ):
1631+ raise TypeError ('fromisoformat: argument must be str' )
1632+
1633+ # Split this at the separator
1634+ dstr = date_string [0 :10 ]
1635+ tstr = date_string [11 :]
1636+
1637+ try :
1638+ date_components = _parse_isoformat_date (dstr )
1639+ except ValueError :
1640+ raise ValueError ('Invalid isoformat string: %s' % date_string )
1641+
1642+ if tstr :
1643+ try :
1644+ time_components = _parse_isoformat_time (tstr )
1645+ except ValueError :
1646+ raise ValueError ('Invalid isoformat string: %s' % date_string )
1647+ else :
1648+ time_components = [0 , 0 , 0 , 0 , None ]
1649+
1650+ return cls (* (date_components + time_components ))
1651+
15001652 def timetuple (self ):
15011653 "Return local time tuple compatible with time.localtime()."
15021654 dst = self .dst ()
@@ -1673,18 +1825,10 @@ def isoformat(self, sep='T', timespec='auto'):
16731825 self ._microsecond , timespec ))
16741826
16751827 off = self .utcoffset ()
1676- if off is not None :
1677- if off .days < 0 :
1678- sign = "-"
1679- off = - off
1680- else :
1681- sign = "+"
1682- hh , mm = divmod (off , timedelta (hours = 1 ))
1683- mm , ss = divmod (mm , timedelta (minutes = 1 ))
1684- s += "%s%02d:%02d" % (sign , hh , mm )
1685- if ss :
1686- assert not ss .microseconds
1687- s += ":%02d" % ss .seconds
1828+ tz = _format_offset (off )
1829+ if tz :
1830+ s += tz
1831+
16881832 return s
16891833
16901834 def __repr__ (self ):
@@ -2275,9 +2419,10 @@ def _name_from_offset(delta):
22752419 _check_date_fields , _check_int_field , _check_time_fields ,
22762420 _check_tzinfo_arg , _check_tzname , _check_utc_offset , _cmp , _cmperror ,
22772421 _date_class , _days_before_month , _days_before_year , _days_in_month ,
2278- _format_time , _is_leap , _isoweek1monday , _math , _ord2ymd ,
2279- _time , _time_class , _tzinfo_class , _wrap_strftime , _ymd2ord ,
2280- _divide_and_round )
2422+ _format_time , _format_offset , _is_leap , _isoweek1monday , _math ,
2423+ _ord2ymd , _time , _time_class , _tzinfo_class , _wrap_strftime , _ymd2ord ,
2424+ _divide_and_round , _parse_isoformat_date , _parse_isoformat_time ,
2425+ _parse_hh_mm_ss_ff )
22812426 # XXX Since import * above excludes names that start with _,
22822427 # docstring does not get overwritten. In the future, it may be
22832428 # appropriate to maintain a single module level docstring and
0 commit comments