@@ -616,6 +616,67 @@ def test_empty_zone(self):
616
616
with self .assertRaises (ValueError ):
617
617
self .klass .from_file (zf )
618
618
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
+
619
680
def construct_zone (self , transitions , after = None , version = 3 ):
620
681
# These are not used for anything, so we're not going to include
621
682
# them for now.
@@ -631,16 +692,25 @@ def construct_zone(self, transitions, after=None, version=3):
631
692
v2_range = (- (2 ** 63 ), 2 ** 63 )
632
693
ranges = [v1_range , v2_range ]
633
694
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
635
702
636
- for zt in transitions :
637
703
if zt .transition :
638
704
trans_time = int (zt .transition_utc .timestamp ())
639
705
else :
640
706
trans_time = None
641
707
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
644
714
645
715
for v , (dt_min , dt_max ) in enumerate (ranges ):
646
716
offsets = offset_lists [v ]
0 commit comments