77
88FUNCTIONS:
99 _getlang -- Figure out what language is being used for the locale
10- strptime -- Calculates the time struct represented by the passed-in string
10+ _strptime -- Calculates the time struct represented by the passed-in string
1111
1212"""
1313import os
@@ -371,7 +371,9 @@ def __init__(self, locale_time=None):
371371 # W is set below by using 'U'
372372 'y' : r"(?P<y>\d\d)" ,
373373 'Y' : r"(?P<Y>\d\d\d\d)" ,
374- 'z' : r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|(?-i:Z))" ,
374+ # See gh-121237: "z" must support colons for backwards compatibility.
375+ 'z' : r"(?P<z>([+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?)|(?-i:Z))?" ,
376+ ':z' : r"(?P<colon_z>([+-]\d\d:[0-5]\d(:[0-5]\d(\.\d{1,6})?)?)|(?-i:Z))?" ,
375377 'A' : self .__seqToRE (self .locale_time .f_weekday , 'A' ),
376378 'a' : self .__seqToRE (self .locale_time .a_weekday , 'a' ),
377379 'B' : self .__seqToRE (_fixmonths (self .locale_time .f_month [1 :]), 'B' ),
@@ -380,7 +382,10 @@ def __init__(self, locale_time=None):
380382 'Z' : self .__seqToRE ((tz for tz_names in self .locale_time .timezone
381383 for tz in tz_names ),
382384 'Z' ),
383- '%' : '%' }
385+ 'n' : r'\s*' ,
386+ 't' : r'\s*' ,
387+ '%' : '%' ,
388+ }
384389 if self .locale_time .LC_alt_digits is None :
385390 for d in 'dmyCHIMS' :
386391 mapping ['O' + d ] = r'(?P<%s>\d\d|\d| \d)' % d
@@ -416,6 +421,8 @@ def __init__(self, locale_time=None):
416421 mapping ['W' ] = mapping ['U' ].replace ('U' , 'W' )
417422
418423 base .__init__ (mapping )
424+ base .__setitem__ ('D' , self .pattern ('%m/%d/%y' ))
425+ base .__setitem__ ('F' , self .pattern ('%Y-%m-%d' ))
419426 base .__setitem__ ('T' , self .pattern ('%H:%M:%S' ))
420427 base .__setitem__ ('R' , self .pattern ('%H:%M' ))
421428 base .__setitem__ ('r' , self .pattern (self .locale_time .LC_time_ampm ))
@@ -457,28 +464,39 @@ def pattern(self, format):
457464 format = re_sub (r'\s+' , r'\\s+' , format )
458465 format = re_sub (r"'" , "['\u02bc ]" , format ) # needed for br_FR
459466 year_in_format = False
460- day_of_month_in_format = False
467+ day_d_in_format = False
468+ day_e_in_format = False
461469 def repl (m ):
462- format_char = m [ 1 ]
463- match format_char :
470+ directive = m . group ()[ 1 :] # exclude `%` symbol
471+ match directive :
464472 case 'Y' | 'y' | 'G' :
465473 nonlocal year_in_format
466474 year_in_format = True
467475 case 'd' :
468- nonlocal day_of_month_in_format
469- day_of_month_in_format = True
470- return self [format_char ]
471- format = re_sub (r'%[-_0^#]*[0-9]*([OE]?\\?.?)' , repl , format )
472- if day_of_month_in_format and not year_in_format :
473- import warnings
474- warnings .warn ("""\
476+ nonlocal day_d_in_format
477+ day_d_in_format = True
478+ case 'e' :
479+ nonlocal day_e_in_format
480+ day_e_in_format = True
481+ return self [directive ]
482+ format = re_sub (r'%[-_0^#]*[0-9]*([OE]?[:\\]?.?)' , repl , format )
483+ if not year_in_format :
484+ if day_d_in_format :
485+ raise ValueError (
486+ "Day of month directive '%d' may not be used without "
487+ "a year directive. Parsing dates involving a day of "
488+ "month without a year is ambiguous and fails to parse "
489+ "leap day. Add a year to the input and format. "
490+ "See https://github.com/python/cpython/issues/70647." )
491+ if day_e_in_format :
492+ import warnings
493+ warnings .warn ("""\
475494 Parsing dates involving a day of month without a year specified is ambiguous
476- and fails to parse leap day. The default behavior will change in Python 3.15
477- to either always raise an exception or to use a different default year (TBD).
478- To avoid trouble, add a specific year to the input & format.
495+ and fails to parse leap day. '%e' without a year will become an error in Python 3.17.
496+ To avoid trouble, add a specific year to the input and format.
479497See https://github.com/python/cpython/issues/70647.""" ,
480- DeprecationWarning ,
481- skip_file_prefixes = (os .path .dirname (__file__ ),))
498+ DeprecationWarning ,
499+ skip_file_prefixes = (os .path .dirname (__file__ ),))
482500 return format
483501
484502 def compile (self , format ):
@@ -514,9 +532,10 @@ def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon):
514532
515533
516534def _strptime (data_string , format = "%a %b %d %H:%M:%S %Y" ):
517- """Return a 2-tuple consisting of a time struct and an int containing
518- the number of microseconds based on the input string and the
519- format string."""
535+ """Return a 3-tuple consisting of a tuple with time components,
536+ an int containing the number of microseconds, and an int
537+ containing the microseconds part of the GMT offset, based on the
538+ input string and the format string."""
520539
521540 for index , arg in enumerate ([data_string , format ]):
522541 if not isinstance (arg , str ):
@@ -555,8 +574,17 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
555574 raise ValueError ("time data %r does not match format %r" %
556575 (data_string , format ))
557576 if len (data_string ) != found .end ():
558- raise ValueError ("unconverted data remains: %s" %
559- data_string [found .end ():])
577+ rest = data_string [found .end ():]
578+ # Specific check for '%:z' directive
579+ if (
580+ "colon_z" in found .re .groupindex
581+ and found .group ("colon_z" ) is not None
582+ and rest [0 ] != ":"
583+ ):
584+ raise ValueError (
585+ f"Missing colon in %:z before '{ rest } ', got '{ data_string } '"
586+ )
587+ raise ValueError ("unconverted data remains: %s" % rest )
560588
561589 iso_year = year = None
562590 month = day = 1
@@ -616,18 +644,18 @@ def parse_int(s):
616644 hour = parse_int (found_dict ['I' ])
617645 ampm = found_dict .get ('p' , '' ).lower ()
618646 # If there was no AM/PM indicator, we'll treat this like AM
619- if ampm in ('' , locale_time .am_pm [0 ]):
620- # We're in AM so the hour is correct unless we're
621- # looking at 12 midnight.
622- # 12 midnight == 12 AM == hour 0
623- if hour == 12 :
624- hour = 0
625- elif ampm == locale_time .am_pm [1 ]:
647+ if ampm == locale_time .am_pm [1 ]:
626648 # We're in PM so we need to add 12 to the hour unless
627649 # we're looking at 12 noon.
628650 # 12 noon == 12 PM == hour 12
629651 if hour != 12 :
630652 hour += 12
653+ else :
654+ # We're in AM so the hour is correct unless we're
655+ # looking at 12 midnight.
656+ # 12 midnight == 12 AM == hour 0
657+ if hour == 12 :
658+ hour = 0
631659 elif group_key == 'M' :
632660 minute = parse_int (found_dict ['M' ])
633661 elif group_key == 'S' :
@@ -662,29 +690,30 @@ def parse_int(s):
662690 week_of_year_start = 0
663691 elif group_key == 'V' :
664692 iso_week = int (found_dict ['V' ])
665- elif group_key == 'z' :
666- z = found_dict ['z' ]
667- if z == 'Z' :
668- gmtoff = 0
669- else :
670- if z [3 ] == ':' :
671- z = z [:3 ] + z [4 :]
672- if len (z ) > 5 :
673- if z [5 ] != ':' :
674- msg = f"Inconsistent use of : in { found_dict ['z' ]} "
675- raise ValueError (msg )
676- z = z [:5 ] + z [6 :]
677- hours = int (z [1 :3 ])
678- minutes = int (z [3 :5 ])
679- seconds = int (z [5 :7 ] or 0 )
680- gmtoff = (hours * 60 * 60 ) + (minutes * 60 ) + seconds
681- gmtoff_remainder = z [8 :]
682- # Pad to always return microseconds.
683- gmtoff_remainder_padding = "0" * (6 - len (gmtoff_remainder ))
684- gmtoff_fraction = int (gmtoff_remainder + gmtoff_remainder_padding )
685- if z .startswith ("-" ):
686- gmtoff = - gmtoff
687- gmtoff_fraction = - gmtoff_fraction
693+ elif group_key in ('z' , 'colon_z' ):
694+ z = found_dict [group_key ]
695+ if z :
696+ if z == 'Z' :
697+ gmtoff = 0
698+ else :
699+ if z [3 ] == ':' :
700+ z = z [:3 ] + z [4 :]
701+ if len (z ) > 5 :
702+ if z [5 ] != ':' :
703+ msg = f"Inconsistent use of : in { found_dict [group_key ]} "
704+ raise ValueError (msg )
705+ z = z [:5 ] + z [6 :]
706+ hours = int (z [1 :3 ])
707+ minutes = int (z [3 :5 ])
708+ seconds = int (z [5 :7 ] or 0 )
709+ gmtoff = (hours * 60 * 60 ) + (minutes * 60 ) + seconds
710+ gmtoff_remainder = z [8 :]
711+ # Pad to always return microseconds.
712+ gmtoff_remainder_padding = "0" * (6 - len (gmtoff_remainder ))
713+ gmtoff_fraction = int (gmtoff_remainder + gmtoff_remainder_padding )
714+ if z .startswith ("-" ):
715+ gmtoff = - gmtoff
716+ gmtoff_fraction = - gmtoff_fraction
688717 elif group_key == 'Z' :
689718 # Since -1 is default value only need to worry about setting tz if
690719 # it can be something other than -1.
0 commit comments