@@ -616,6 +616,67 @@ def test_empty_zone(self):
616616 with self .assertRaises (ValueError ):
617617 self .klass .from_file (zf )
618618
619+ def test_zone_very_large_timestamp (self ):
620+ """Test when a transition is in the far past or future.
621+
622+ Particularly, this is a concern if something:
623+
624+ 1. Attempts to call ``datetime.timestamp`` for a datetime outside
625+ of ``[datetime.min, datetime.max]``.
626+ 2. Attempts to construct a timedelta outside of
627+ ``[timedelta.min, timedelta.max]``.
628+
629+ This actually occurs "in the wild", as some time zones on Ubuntu (at
630+ least as of 2020) have an initial transition added at ``-2**58``.
631+ """
632+
633+ LMT = ZoneOffset ("LMT" , timedelta (seconds = - 968 ))
634+ GMT = ZoneOffset ("GMT" , ZERO )
635+
636+ transitions = [
637+ (- (1 << 62 ), LMT , LMT ),
638+ ZoneTransition (datetime (1912 , 1 , 1 ), LMT , GMT ),
639+ ((1 << 62 ), GMT , GMT ),
640+ ]
641+
642+ after = "GMT0"
643+
644+ zf = self .construct_zone (transitions , after )
645+ zi = self .klass .from_file (zf , key = "Africa/Abidjan" )
646+
647+ offset_cases = [
648+ (datetime .min , LMT ),
649+ (datetime .max , GMT ),
650+ (datetime (1911 , 12 , 31 ), LMT ),
651+ (datetime (1912 , 1 , 2 ), GMT ),
652+ ]
653+
654+ for dt_naive , offset in offset_cases :
655+ dt = dt_naive .replace (tzinfo = zi )
656+ with self .subTest (name = "offset" , dt = dt , offset = offset ):
657+ self .assertEqual (dt .tzname (), offset .tzname )
658+ self .assertEqual (dt .utcoffset (), offset .utcoffset )
659+ self .assertEqual (dt .dst (), offset .dst )
660+
661+ utc_cases = [
662+ (datetime .min , datetime .min + timedelta (seconds = 968 )),
663+ (datetime (1898 , 12 , 31 , 23 , 43 , 52 ), datetime (1899 , 1 , 1 )),
664+ (
665+ datetime (1911 , 12 , 31 , 23 , 59 , 59 , 999999 ),
666+ datetime (1912 , 1 , 1 , 0 , 16 , 7 , 999999 ),
667+ ),
668+ (datetime (1912 , 1 , 1 , 0 , 16 , 8 ), datetime (1912 , 1 , 1 , 0 , 16 , 8 )),
669+ (datetime (1970 , 1 , 1 ), datetime (1970 , 1 , 1 )),
670+ (datetime .max , datetime .max ),
671+ ]
672+
673+ for naive_dt , naive_dt_utc in utc_cases :
674+ dt = naive_dt .replace (tzinfo = zi )
675+ dt_utc = naive_dt_utc .replace (tzinfo = timezone .utc )
676+
677+ self .assertEqual (dt_utc .astimezone (zi ), dt )
678+ self .assertEqual (dt , dt_utc )
679+
619680 def construct_zone (self , transitions , after = None , version = 3 ):
620681 # These are not used for anything, so we're not going to include
621682 # them for now.
@@ -631,16 +692,25 @@ def construct_zone(self, transitions, after=None, version=3):
631692 v2_range = (- (2 ** 63 ), 2 ** 63 )
632693 ranges = [v1_range , v2_range ]
633694
634- transitions .sort (key = lambda x : x .transition )
695+ def zt_as_tuple (zt ):
696+ # zt may be a tuple (timestamp, offset_before, offset_after) or
697+ # a ZoneTransition object — this is to allow the timestamp to be
698+ # values that are outside the valid range for datetimes but still
699+ # valid 64-bit timestamps.
700+ if isinstance (zt , tuple ):
701+ return zt
635702
636- for zt in transitions :
637703 if zt .transition :
638704 trans_time = int (zt .transition_utc .timestamp ())
639705 else :
640706 trans_time = None
641707
642- offset_before = zt .offset_before
643- offset_after = zt .offset_after
708+ return (trans_time , zt .offset_before , zt .offset_after )
709+
710+ transitions = sorted (map (zt_as_tuple , transitions ), key = lambda x : x [0 ])
711+
712+ for zt in transitions :
713+ trans_time , offset_before , offset_after = zt
644714
645715 for v , (dt_min , dt_max ) in enumerate (ranges ):
646716 offsets = offset_lists [v ]
0 commit comments