1from __future__ import annotations
2
3import base64
4import contextlib
5import dataclasses
6import importlib.metadata
7import io
8import json
9import os
10import pathlib
11import pickle
12import re
13import shutil
14import struct
15import tempfile
16import unittest
17from datetime import date, datetime, time, timedelta, timezone
18from functools import cached_property
19
20from test.test_zoneinfo import _support as test_support
21from test.test_zoneinfo._support import OS_ENV_LOCK, TZPATH_TEST_LOCK, ZoneInfoTestBase
22from test.support.import_helper import import_module
23
24lzma = import_module('lzma')
25py_zoneinfo, c_zoneinfo = test_support.get_modules()
26
27try:
28    importlib.metadata.metadata("tzdata")
29    HAS_TZDATA_PKG = True
30except importlib.metadata.PackageNotFoundError:
31    HAS_TZDATA_PKG = False
32
33ZONEINFO_DATA = None
34ZONEINFO_DATA_V1 = None
35TEMP_DIR = None
36DATA_DIR = pathlib.Path(__file__).parent / "data"
37ZONEINFO_JSON = DATA_DIR / "zoneinfo_data.json"
38
39# Useful constants
40ZERO = timedelta(0)
41ONE_H = timedelta(hours=1)
42
43
44def setUpModule():
45    global TEMP_DIR
46    global ZONEINFO_DATA
47    global ZONEINFO_DATA_V1
48
49    TEMP_DIR = pathlib.Path(tempfile.mkdtemp(prefix="zoneinfo"))
50    ZONEINFO_DATA = ZoneInfoData(ZONEINFO_JSON, TEMP_DIR / "v2")
51    ZONEINFO_DATA_V1 = ZoneInfoData(ZONEINFO_JSON, TEMP_DIR / "v1", v1=True)
52
53
54def tearDownModule():
55    shutil.rmtree(TEMP_DIR)
56
57
58class TzPathUserMixin:
59    """
60    Adds a setUp() and tearDown() to make TZPATH manipulations thread-safe.
61
62    Any tests that require manipulation of the TZPATH global are necessarily
63    thread unsafe, so we will acquire a lock and reset the TZPATH variable
64    to the default state before each test and release the lock after the test
65    is through.
66    """
67
68    @property
69    def tzpath(self):  # pragma: nocover
70        return None
71
72    @property
73    def block_tzdata(self):
74        return True
75
76    def setUp(self):
77        with contextlib.ExitStack() as stack:
78            stack.enter_context(
79                self.tzpath_context(
80                    self.tzpath,
81                    block_tzdata=self.block_tzdata,
82                    lock=TZPATH_TEST_LOCK,
83                )
84            )
85            self.addCleanup(stack.pop_all().close)
86
87        super().setUp()
88
89
90class DatetimeSubclassMixin:
91    """
92    Replaces all ZoneTransition transition dates with a datetime subclass.
93    """
94
95    class DatetimeSubclass(datetime):
96        @classmethod
97        def from_datetime(cls, dt):
98            return cls(
99                dt.year,
100                dt.month,
101                dt.day,
102                dt.hour,
103                dt.minute,
104                dt.second,
105                dt.microsecond,
106                tzinfo=dt.tzinfo,
107                fold=dt.fold,
108            )
109
110    def load_transition_examples(self, key):
111        transition_examples = super().load_transition_examples(key)
112        for zt in transition_examples:
113            dt = zt.transition
114            new_dt = self.DatetimeSubclass.from_datetime(dt)
115            new_zt = dataclasses.replace(zt, transition=new_dt)
116            yield new_zt
117
118
119class ZoneInfoTest(TzPathUserMixin, ZoneInfoTestBase):
120    module = py_zoneinfo
121    class_name = "ZoneInfo"
122
123    def setUp(self):
124        super().setUp()
125
126        # This is necessary because various subclasses pull from different
127        # data sources (e.g. tzdata, V1 files, etc).
128        self.klass.clear_cache()
129
130    @property
131    def zoneinfo_data(self):
132        return ZONEINFO_DATA
133
134    @property
135    def tzpath(self):
136        return [self.zoneinfo_data.tzpath]
137
138    def zone_from_key(self, key):
139        return self.klass(key)
140
141    def zones(self):
142        return ZoneDumpData.transition_keys()
143
144    def fixed_offset_zones(self):
145        return ZoneDumpData.fixed_offset_zones()
146
147    def load_transition_examples(self, key):
148        return ZoneDumpData.load_transition_examples(key)
149
150    def test_str(self):
151        # Zones constructed with a key must have str(zone) == key
152        for key in self.zones():
153            with self.subTest(key):
154                zi = self.zone_from_key(key)
155
156                self.assertEqual(str(zi), key)
157
158        # Zones with no key constructed should have str(zone) == repr(zone)
159        file_key = self.zoneinfo_data.keys[0]
160        file_path = self.zoneinfo_data.path_from_key(file_key)
161
162        with open(file_path, "rb") as f:
163            with self.subTest(test_name="Repr test", path=file_path):
164                zi_ff = self.klass.from_file(f)
165                self.assertEqual(str(zi_ff), repr(zi_ff))
166
167    def test_repr(self):
168        # The repr is not guaranteed, but I think we can insist that it at
169        # least contain the name of the class.
170        key = next(iter(self.zones()))
171
172        zi = self.klass(key)
173        class_name = self.class_name
174        with self.subTest(name="from key"):
175            self.assertRegex(repr(zi), class_name)
176
177        file_key = self.zoneinfo_data.keys[0]
178        file_path = self.zoneinfo_data.path_from_key(file_key)
179        with open(file_path, "rb") as f:
180            zi_ff = self.klass.from_file(f, key=file_key)
181
182        with self.subTest(name="from file with key"):
183            self.assertRegex(repr(zi_ff), class_name)
184
185        with open(file_path, "rb") as f:
186            zi_ff_nk = self.klass.from_file(f)
187
188        with self.subTest(name="from file without key"):
189            self.assertRegex(repr(zi_ff_nk), class_name)
190
191    def test_key_attribute(self):
192        key = next(iter(self.zones()))
193
194        def from_file_nokey(key):
195            with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
196                return self.klass.from_file(f)
197
198        constructors = (
199            ("Primary constructor", self.klass, key),
200            ("no_cache", self.klass.no_cache, key),
201            ("from_file", from_file_nokey, None),
202        )
203
204        for msg, constructor, expected in constructors:
205            zi = constructor(key)
206
207            # Ensure that the key attribute is set to the input to ``key``
208            with self.subTest(msg):
209                self.assertEqual(zi.key, expected)
210
211            # Ensure that the key attribute is read-only
212            with self.subTest(f"{msg}: readonly"):
213                with self.assertRaises(AttributeError):
214                    zi.key = "Some/Value"
215
216    def test_bad_keys(self):
217        bad_keys = [
218            "Eurasia/Badzone",  # Plausible but does not exist
219            "BZQ",
220            "America.Los_Angeles",
221            "����",  # Non-ascii
222            "America/New\ud800York",  # Contains surrogate character
223        ]
224
225        for bad_key in bad_keys:
226            with self.assertRaises(self.module.ZoneInfoNotFoundError):
227                self.klass(bad_key)
228
229    def test_bad_keys_paths(self):
230        bad_keys = [
231            "/America/Los_Angeles",  # Absolute path
232            "America/Los_Angeles/",  # Trailing slash - not normalized
233            "../zoneinfo/America/Los_Angeles",  # Traverses above TZPATH
234            "America/../America/Los_Angeles",  # Not normalized
235            "America/./Los_Angeles",
236        ]
237
238        for bad_key in bad_keys:
239            with self.assertRaises(ValueError):
240                self.klass(bad_key)
241
242    def test_bad_zones(self):
243        bad_zones = [
244            b"",  # Empty file
245            b"AAAA3" + b" " * 15,  # Bad magic
246        ]
247
248        for bad_zone in bad_zones:
249            fobj = io.BytesIO(bad_zone)
250            with self.assertRaises(ValueError):
251                self.klass.from_file(fobj)
252
253    def test_fromutc_errors(self):
254        key = next(iter(self.zones()))
255        zone = self.zone_from_key(key)
256
257        bad_values = [
258            (datetime(2019, 1, 1, tzinfo=timezone.utc), ValueError),
259            (datetime(2019, 1, 1), ValueError),
260            (date(2019, 1, 1), TypeError),
261            (time(0), TypeError),
262            (0, TypeError),
263            ("2019-01-01", TypeError),
264        ]
265
266        for val, exc_type in bad_values:
267            with self.subTest(val=val):
268                with self.assertRaises(exc_type):
269                    zone.fromutc(val)
270
271    def test_utc(self):
272        zi = self.klass("UTC")
273        dt = datetime(2020, 1, 1, tzinfo=zi)
274
275        self.assertEqual(dt.utcoffset(), ZERO)
276        self.assertEqual(dt.dst(), ZERO)
277        self.assertEqual(dt.tzname(), "UTC")
278
279    def test_unambiguous(self):
280        test_cases = []
281        for key in self.zones():
282            for zone_transition in self.load_transition_examples(key):
283                test_cases.append(
284                    (
285                        key,
286                        zone_transition.transition - timedelta(days=2),
287                        zone_transition.offset_before,
288                    )
289                )
290
291                test_cases.append(
292                    (
293                        key,
294                        zone_transition.transition + timedelta(days=2),
295                        zone_transition.offset_after,
296                    )
297                )
298
299        for key, dt, offset in test_cases:
300            with self.subTest(key=key, dt=dt, offset=offset):
301                tzi = self.zone_from_key(key)
302                dt = dt.replace(tzinfo=tzi)
303
304                self.assertEqual(dt.tzname(), offset.tzname, dt)
305                self.assertEqual(dt.utcoffset(), offset.utcoffset, dt)
306                self.assertEqual(dt.dst(), offset.dst, dt)
307
308    def test_folds_and_gaps(self):
309        test_cases = []
310        for key in self.zones():
311            tests = {"folds": [], "gaps": []}
312            for zt in self.load_transition_examples(key):
313                if zt.fold:
314                    test_group = tests["folds"]
315                elif zt.gap:
316                    test_group = tests["gaps"]
317                else:
318                    # Assign a random variable here to disable the peephole
319                    # optimizer so that coverage can see this line.
320                    # See bpo-2506 for more information.
321                    no_peephole_opt = None
322                    continue
323
324                # Cases are of the form key, dt, fold, offset
325                dt = zt.anomaly_start - timedelta(seconds=1)
326                test_group.append((dt, 0, zt.offset_before))
327                test_group.append((dt, 1, zt.offset_before))
328
329                dt = zt.anomaly_start
330                test_group.append((dt, 0, zt.offset_before))
331                test_group.append((dt, 1, zt.offset_after))
332
333                dt = zt.anomaly_start + timedelta(seconds=1)
334                test_group.append((dt, 0, zt.offset_before))
335                test_group.append((dt, 1, zt.offset_after))
336
337                dt = zt.anomaly_end - timedelta(seconds=1)
338                test_group.append((dt, 0, zt.offset_before))
339                test_group.append((dt, 1, zt.offset_after))
340
341                dt = zt.anomaly_end
342                test_group.append((dt, 0, zt.offset_after))
343                test_group.append((dt, 1, zt.offset_after))
344
345                dt = zt.anomaly_end + timedelta(seconds=1)
346                test_group.append((dt, 0, zt.offset_after))
347                test_group.append((dt, 1, zt.offset_after))
348
349            for grp, test_group in tests.items():
350                test_cases.append(((key, grp), test_group))
351
352        for (key, grp), tests in test_cases:
353            with self.subTest(key=key, grp=grp):
354                tzi = self.zone_from_key(key)
355
356                for dt, fold, offset in tests:
357                    dt = dt.replace(fold=fold, tzinfo=tzi)
358
359                    self.assertEqual(dt.tzname(), offset.tzname, dt)
360                    self.assertEqual(dt.utcoffset(), offset.utcoffset, dt)
361                    self.assertEqual(dt.dst(), offset.dst, dt)
362
363    def test_folds_from_utc(self):
364        for key in self.zones():
365            zi = self.zone_from_key(key)
366            with self.subTest(key=key):
367                for zt in self.load_transition_examples(key):
368                    if not zt.fold:
369                        continue
370
371                    dt_utc = zt.transition_utc
372                    dt_before_utc = dt_utc - timedelta(seconds=1)
373                    dt_after_utc = dt_utc + timedelta(seconds=1)
374
375                    dt_before = dt_before_utc.astimezone(zi)
376                    self.assertEqual(dt_before.fold, 0, (dt_before, dt_utc))
377
378                    dt_after = dt_after_utc.astimezone(zi)
379                    self.assertEqual(dt_after.fold, 1, (dt_after, dt_utc))
380
381    def test_time_variable_offset(self):
382        # self.zones() only ever returns variable-offset zones
383        for key in self.zones():
384            zi = self.zone_from_key(key)
385            t = time(11, 15, 1, 34471, tzinfo=zi)
386
387            with self.subTest(key=key):
388                self.assertIs(t.tzname(), None)
389                self.assertIs(t.utcoffset(), None)
390                self.assertIs(t.dst(), None)
391
392    def test_time_fixed_offset(self):
393        for key, offset in self.fixed_offset_zones():
394            zi = self.zone_from_key(key)
395
396            t = time(11, 15, 1, 34471, tzinfo=zi)
397
398            with self.subTest(key=key):
399                self.assertEqual(t.tzname(), offset.tzname)
400                self.assertEqual(t.utcoffset(), offset.utcoffset)
401                self.assertEqual(t.dst(), offset.dst)
402
403
404class CZoneInfoTest(ZoneInfoTest):
405    module = c_zoneinfo
406
407    def test_fold_mutate(self):
408        """Test that fold isn't mutated when no change is necessary.
409
410        The underlying C API is capable of mutating datetime objects, and
411        may rely on the fact that addition of a datetime object returns a
412        new datetime; this test ensures that the input datetime to fromutc
413        is not mutated.
414        """
415
416        def to_subclass(dt):
417            class SameAddSubclass(type(dt)):
418                def __add__(self, other):
419                    if other == timedelta(0):
420                        return self
421
422                    return super().__add__(other)  # pragma: nocover
423
424            return SameAddSubclass(
425                dt.year,
426                dt.month,
427                dt.day,
428                dt.hour,
429                dt.minute,
430                dt.second,
431                dt.microsecond,
432                fold=dt.fold,
433                tzinfo=dt.tzinfo,
434            )
435
436        subclass = [False, True]
437
438        key = "Europe/London"
439        zi = self.zone_from_key(key)
440        for zt in self.load_transition_examples(key):
441            if zt.fold and zt.offset_after.utcoffset == ZERO:
442                example = zt.transition_utc.replace(tzinfo=zi)
443                break
444
445        for subclass in [False, True]:
446            if subclass:
447                dt = to_subclass(example)
448            else:
449                dt = example
450
451            with self.subTest(subclass=subclass):
452                dt_fromutc = zi.fromutc(dt)
453
454                self.assertEqual(dt_fromutc.fold, 1)
455                self.assertEqual(dt.fold, 0)
456
457
458class ZoneInfoDatetimeSubclassTest(DatetimeSubclassMixin, ZoneInfoTest):
459    pass
460
461
462class CZoneInfoDatetimeSubclassTest(DatetimeSubclassMixin, CZoneInfoTest):
463    pass
464
465
466class ZoneInfoSubclassTest(ZoneInfoTest):
467    @classmethod
468    def setUpClass(cls):
469        super().setUpClass()
470
471        class ZISubclass(cls.klass):
472            pass
473
474        cls.class_name = "ZISubclass"
475        cls.parent_klass = cls.klass
476        cls.klass = ZISubclass
477
478    def test_subclass_own_cache(self):
479        base_obj = self.parent_klass("Europe/London")
480        sub_obj = self.klass("Europe/London")
481
482        self.assertIsNot(base_obj, sub_obj)
483        self.assertIsInstance(base_obj, self.parent_klass)
484        self.assertIsInstance(sub_obj, self.klass)
485
486
487class CZoneInfoSubclassTest(ZoneInfoSubclassTest):
488    module = c_zoneinfo
489
490
491class ZoneInfoV1Test(ZoneInfoTest):
492    @property
493    def zoneinfo_data(self):
494        return ZONEINFO_DATA_V1
495
496    def load_transition_examples(self, key):
497        # We will discard zdump examples outside the range epoch +/- 2**31,
498        # because they are not well-supported in Version 1 files.
499        epoch = datetime(1970, 1, 1)
500        max_offset_32 = timedelta(seconds=2 ** 31)
501        min_dt = epoch - max_offset_32
502        max_dt = epoch + max_offset_32
503
504        for zt in ZoneDumpData.load_transition_examples(key):
505            if min_dt <= zt.transition <= max_dt:
506                yield zt
507
508
509class CZoneInfoV1Test(ZoneInfoV1Test):
510    module = c_zoneinfo
511
512
513@unittest.skipIf(
514    not HAS_TZDATA_PKG, "Skipping tzdata-specific tests: tzdata not installed"
515)
516class TZDataTests(ZoneInfoTest):
517    """
518    Runs all the ZoneInfoTest tests, but against the tzdata package
519
520    NOTE: The ZoneDumpData has frozen test data, but tzdata will update, so
521    some of the tests (particularly those related to the far future) may break
522    in the event that the time zone policies in the relevant time zones change.
523    """
524
525    @property
526    def tzpath(self):
527        return []
528
529    @property
530    def block_tzdata(self):
531        return False
532
533    def zone_from_key(self, key):
534        return self.klass(key=key)
535
536
537@unittest.skipIf(
538    not HAS_TZDATA_PKG, "Skipping tzdata-specific tests: tzdata not installed"
539)
540class CTZDataTests(TZDataTests):
541    module = c_zoneinfo
542
543
544class WeirdZoneTest(ZoneInfoTestBase):
545    module = py_zoneinfo
546
547    def test_one_transition(self):
548        LMT = ZoneOffset("LMT", -timedelta(hours=6, minutes=31, seconds=2))
549        STD = ZoneOffset("STD", -timedelta(hours=6))
550
551        transitions = [
552            ZoneTransition(datetime(1883, 6, 9, 14), LMT, STD),
553        ]
554
555        after = "STD6"
556
557        zf = self.construct_zone(transitions, after)
558        zi = self.klass.from_file(zf)
559
560        dt0 = datetime(1883, 6, 9, 1, tzinfo=zi)
561        dt1 = datetime(1883, 6, 10, 1, tzinfo=zi)
562
563        for dt, offset in [(dt0, LMT), (dt1, STD)]:
564            with self.subTest(name="local", dt=dt):
565                self.assertEqual(dt.tzname(), offset.tzname)
566                self.assertEqual(dt.utcoffset(), offset.utcoffset)
567                self.assertEqual(dt.dst(), offset.dst)
568
569        dts = [
570            (
571                datetime(1883, 6, 9, 1, tzinfo=zi),
572                datetime(1883, 6, 9, 7, 31, 2, tzinfo=timezone.utc),
573            ),
574            (
575                datetime(2010, 4, 1, 12, tzinfo=zi),
576                datetime(2010, 4, 1, 18, tzinfo=timezone.utc),
577            ),
578        ]
579
580        for dt_local, dt_utc in dts:
581            with self.subTest(name="fromutc", dt=dt_local):
582                dt_actual = dt_utc.astimezone(zi)
583                self.assertEqual(dt_actual, dt_local)
584
585                dt_utc_actual = dt_local.astimezone(timezone.utc)
586                self.assertEqual(dt_utc_actual, dt_utc)
587
588    def test_one_zone_dst(self):
589        DST = ZoneOffset("DST", ONE_H, ONE_H)
590        transitions = [
591            ZoneTransition(datetime(1970, 1, 1), DST, DST),
592        ]
593
594        after = "STD0DST-1,0/0,J365/25"
595
596        zf = self.construct_zone(transitions, after)
597        zi = self.klass.from_file(zf)
598
599        dts = [
600            datetime(1900, 3, 1),
601            datetime(1965, 9, 12),
602            datetime(1970, 1, 1),
603            datetime(2010, 11, 3),
604            datetime(2040, 1, 1),
605        ]
606
607        for dt in dts:
608            dt = dt.replace(tzinfo=zi)
609            with self.subTest(dt=dt):
610                self.assertEqual(dt.tzname(), DST.tzname)
611                self.assertEqual(dt.utcoffset(), DST.utcoffset)
612                self.assertEqual(dt.dst(), DST.dst)
613
614    def test_no_tz_str(self):
615        STD = ZoneOffset("STD", ONE_H, ZERO)
616        DST = ZoneOffset("DST", 2 * ONE_H, ONE_H)
617
618        transitions = []
619        for year in range(1996, 2000):
620            transitions.append(
621                ZoneTransition(datetime(year, 3, 1, 2), STD, DST)
622            )
623            transitions.append(
624                ZoneTransition(datetime(year, 11, 1, 2), DST, STD)
625            )
626
627        after = ""
628
629        zf = self.construct_zone(transitions, after)
630
631        # According to RFC 8536, local times after the last transition time
632        # with an empty TZ string are unspecified. We will go with "hold the
633        # last transition", but the most we should promise is "doesn't crash."
634        zi = self.klass.from_file(zf)
635
636        cases = [
637            (datetime(1995, 1, 1), STD),
638            (datetime(1996, 4, 1), DST),
639            (datetime(1996, 11, 2), STD),
640            (datetime(2001, 1, 1), STD),
641        ]
642
643        for dt, offset in cases:
644            dt = dt.replace(tzinfo=zi)
645            with self.subTest(dt=dt):
646                self.assertEqual(dt.tzname(), offset.tzname)
647                self.assertEqual(dt.utcoffset(), offset.utcoffset)
648                self.assertEqual(dt.dst(), offset.dst)
649
650        # Test that offsets return None when using a datetime.time
651        t = time(0, tzinfo=zi)
652        with self.subTest("Testing datetime.time"):
653            self.assertIs(t.tzname(), None)
654            self.assertIs(t.utcoffset(), None)
655            self.assertIs(t.dst(), None)
656
657    def test_tz_before_only(self):
658        # From RFC 8536 Section 3.2:
659        #
660        #   If there are no transitions, local time for all timestamps is
661        #   specified by the TZ string in the footer if present and nonempty;
662        #   otherwise, it is specified by time type 0.
663
664        offsets = [
665            ZoneOffset("STD", ZERO, ZERO),
666            ZoneOffset("DST", ONE_H, ONE_H),
667        ]
668
669        for offset in offsets:
670            # Phantom transition to set time type 0.
671            transitions = [
672                ZoneTransition(None, offset, offset),
673            ]
674
675            after = ""
676
677            zf = self.construct_zone(transitions, after)
678            zi = self.klass.from_file(zf)
679
680            dts = [
681                datetime(1900, 1, 1),
682                datetime(1970, 1, 1),
683                datetime(2000, 1, 1),
684            ]
685
686            for dt in dts:
687                dt = dt.replace(tzinfo=zi)
688                with self.subTest(offset=offset, dt=dt):
689                    self.assertEqual(dt.tzname(), offset.tzname)
690                    self.assertEqual(dt.utcoffset(), offset.utcoffset)
691                    self.assertEqual(dt.dst(), offset.dst)
692
693    def test_empty_zone(self):
694        zf = self.construct_zone([], "")
695
696        with self.assertRaises(ValueError):
697            self.klass.from_file(zf)
698
699    def test_zone_very_large_timestamp(self):
700        """Test when a transition is in the far past or future.
701
702        Particularly, this is a concern if something:
703
704            1. Attempts to call ``datetime.timestamp`` for a datetime outside
705               of ``[datetime.min, datetime.max]``.
706            2. Attempts to construct a timedelta outside of
707               ``[timedelta.min, timedelta.max]``.
708
709        This actually occurs "in the wild", as some time zones on Ubuntu (at
710        least as of 2020) have an initial transition added at ``-2**58``.
711        """
712
713        LMT = ZoneOffset("LMT", timedelta(seconds=-968))
714        GMT = ZoneOffset("GMT", ZERO)
715
716        transitions = [
717            (-(1 << 62), LMT, LMT),
718            ZoneTransition(datetime(1912, 1, 1), LMT, GMT),
719            ((1 << 62), GMT, GMT),
720        ]
721
722        after = "GMT0"
723
724        zf = self.construct_zone(transitions, after)
725        zi = self.klass.from_file(zf, key="Africa/Abidjan")
726
727        offset_cases = [
728            (datetime.min, LMT),
729            (datetime.max, GMT),
730            (datetime(1911, 12, 31), LMT),
731            (datetime(1912, 1, 2), GMT),
732        ]
733
734        for dt_naive, offset in offset_cases:
735            dt = dt_naive.replace(tzinfo=zi)
736            with self.subTest(name="offset", dt=dt, offset=offset):
737                self.assertEqual(dt.tzname(), offset.tzname)
738                self.assertEqual(dt.utcoffset(), offset.utcoffset)
739                self.assertEqual(dt.dst(), offset.dst)
740
741        utc_cases = [
742            (datetime.min, datetime.min + timedelta(seconds=968)),
743            (datetime(1898, 12, 31, 23, 43, 52), datetime(1899, 1, 1)),
744            (
745                datetime(1911, 12, 31, 23, 59, 59, 999999),
746                datetime(1912, 1, 1, 0, 16, 7, 999999),
747            ),
748            (datetime(1912, 1, 1, 0, 16, 8), datetime(1912, 1, 1, 0, 16, 8)),
749            (datetime(1970, 1, 1), datetime(1970, 1, 1)),
750            (datetime.max, datetime.max),
751        ]
752
753        for naive_dt, naive_dt_utc in utc_cases:
754            dt = naive_dt.replace(tzinfo=zi)
755            dt_utc = naive_dt_utc.replace(tzinfo=timezone.utc)
756
757            self.assertEqual(dt_utc.astimezone(zi), dt)
758            self.assertEqual(dt, dt_utc)
759
760    def test_fixed_offset_phantom_transition(self):
761        UTC = ZoneOffset("UTC", ZERO, ZERO)
762
763        transitions = [ZoneTransition(datetime(1970, 1, 1), UTC, UTC)]
764
765        after = "UTC0"
766        zf = self.construct_zone(transitions, after)
767        zi = self.klass.from_file(zf, key="UTC")
768
769        dt = datetime(2020, 1, 1, tzinfo=zi)
770        with self.subTest("datetime.datetime"):
771            self.assertEqual(dt.tzname(), UTC.tzname)
772            self.assertEqual(dt.utcoffset(), UTC.utcoffset)
773            self.assertEqual(dt.dst(), UTC.dst)
774
775        t = time(0, tzinfo=zi)
776        with self.subTest("datetime.time"):
777            self.assertEqual(t.tzname(), UTC.tzname)
778            self.assertEqual(t.utcoffset(), UTC.utcoffset)
779            self.assertEqual(t.dst(), UTC.dst)
780
781    def construct_zone(self, transitions, after=None, version=3):
782        # These are not used for anything, so we're not going to include
783        # them for now.
784        isutc = []
785        isstd = []
786        leap_seconds = []
787
788        offset_lists = [[], []]
789        trans_times_lists = [[], []]
790        trans_idx_lists = [[], []]
791
792        v1_range = (-(2 ** 31), 2 ** 31)
793        v2_range = (-(2 ** 63), 2 ** 63)
794        ranges = [v1_range, v2_range]
795
796        def zt_as_tuple(zt):
797            # zt may be a tuple (timestamp, offset_before, offset_after) or
798            # a ZoneTransition object — this is to allow the timestamp to be
799            # values that are outside the valid range for datetimes but still
800            # valid 64-bit timestamps.
801            if isinstance(zt, tuple):
802                return zt
803
804            if zt.transition:
805                trans_time = int(zt.transition_utc.timestamp())
806            else:
807                trans_time = None
808
809            return (trans_time, zt.offset_before, zt.offset_after)
810
811        transitions = sorted(map(zt_as_tuple, transitions), key=lambda x: x[0])
812
813        for zt in transitions:
814            trans_time, offset_before, offset_after = zt
815
816            for v, (dt_min, dt_max) in enumerate(ranges):
817                offsets = offset_lists[v]
818                trans_times = trans_times_lists[v]
819                trans_idx = trans_idx_lists[v]
820
821                if trans_time is not None and not (
822                    dt_min <= trans_time <= dt_max
823                ):
824                    continue
825
826                if offset_before not in offsets:
827                    offsets.append(offset_before)
828
829                if offset_after not in offsets:
830                    offsets.append(offset_after)
831
832                if trans_time is not None:
833                    trans_times.append(trans_time)
834                    trans_idx.append(offsets.index(offset_after))
835
836        isutcnt = len(isutc)
837        isstdcnt = len(isstd)
838        leapcnt = len(leap_seconds)
839
840        zonefile = io.BytesIO()
841
842        time_types = ("l", "q")
843        for v in range(min((version, 2))):
844            offsets = offset_lists[v]
845            trans_times = trans_times_lists[v]
846            trans_idx = trans_idx_lists[v]
847            time_type = time_types[v]
848
849            # Translate the offsets into something closer to the C values
850            abbrstr = bytearray()
851            ttinfos = []
852
853            for offset in offsets:
854                utcoff = int(offset.utcoffset.total_seconds())
855                isdst = bool(offset.dst)
856                abbrind = len(abbrstr)
857
858                ttinfos.append((utcoff, isdst, abbrind))
859                abbrstr += offset.tzname.encode("ascii") + b"\x00"
860            abbrstr = bytes(abbrstr)
861
862            typecnt = len(offsets)
863            timecnt = len(trans_times)
864            charcnt = len(abbrstr)
865
866            # Write the header
867            zonefile.write(b"TZif")
868            zonefile.write(b"%d" % version)
869            zonefile.write(b" " * 15)
870            zonefile.write(
871                struct.pack(
872                    ">6l", isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt
873                )
874            )
875
876            # Now the transition data
877            zonefile.write(struct.pack(f">{timecnt}{time_type}", *trans_times))
878            zonefile.write(struct.pack(f">{timecnt}B", *trans_idx))
879
880            for ttinfo in ttinfos:
881                zonefile.write(struct.pack(">lbb", *ttinfo))
882
883            zonefile.write(bytes(abbrstr))
884
885            # Now the metadata and leap seconds
886            zonefile.write(struct.pack(f"{isutcnt}b", *isutc))
887            zonefile.write(struct.pack(f"{isstdcnt}b", *isstd))
888            zonefile.write(struct.pack(f">{leapcnt}l", *leap_seconds))
889
890            # Finally we write the TZ string if we're writing a Version 2+ file
891            if v > 0:
892                zonefile.write(b"\x0A")
893                zonefile.write(after.encode("ascii"))
894                zonefile.write(b"\x0A")
895
896        zonefile.seek(0)
897        return zonefile
898
899
900class CWeirdZoneTest(WeirdZoneTest):
901    module = c_zoneinfo
902
903
904class TZStrTest(ZoneInfoTestBase):
905    module = py_zoneinfo
906
907    NORMAL = 0
908    FOLD = 1
909    GAP = 2
910
911    @classmethod
912    def setUpClass(cls):
913        super().setUpClass()
914
915        cls._populate_test_cases()
916        cls.populate_tzstr_header()
917
918    @classmethod
919    def populate_tzstr_header(cls):
920        out = bytearray()
921        # The TZif format always starts with a Version 1 file followed by
922        # the Version 2+ file. In this case, we have no transitions, just
923        # the tzstr in the footer, so up to the footer, the files are
924        # identical and we can just write the same file twice in a row.
925        for _ in range(2):
926            out += b"TZif"  # Magic value
927            out += b"3"  # Version
928            out += b" " * 15  # Reserved
929
930            # We will not write any of the manual transition parts
931            out += struct.pack(">6l", 0, 0, 0, 0, 0, 0)
932
933        cls._tzif_header = bytes(out)
934
935    def zone_from_tzstr(self, tzstr):
936        """Creates a zoneinfo file following a POSIX rule."""
937        zonefile = io.BytesIO(self._tzif_header)
938        zonefile.seek(0, 2)
939
940        # Write the footer
941        zonefile.write(b"\x0A")
942        zonefile.write(tzstr.encode("ascii"))
943        zonefile.write(b"\x0A")
944
945        zonefile.seek(0)
946
947        return self.klass.from_file(zonefile, key=tzstr)
948
949    def test_tzstr_localized(self):
950        for tzstr, cases in self.test_cases.items():
951            with self.subTest(tzstr=tzstr):
952                zi = self.zone_from_tzstr(tzstr)
953
954            for dt_naive, offset, _ in cases:
955                dt = dt_naive.replace(tzinfo=zi)
956
957                with self.subTest(tzstr=tzstr, dt=dt, offset=offset):
958                    self.assertEqual(dt.tzname(), offset.tzname)
959                    self.assertEqual(dt.utcoffset(), offset.utcoffset)
960                    self.assertEqual(dt.dst(), offset.dst)
961
962    def test_tzstr_from_utc(self):
963        for tzstr, cases in self.test_cases.items():
964            with self.subTest(tzstr=tzstr):
965                zi = self.zone_from_tzstr(tzstr)
966
967            for dt_naive, offset, dt_type in cases:
968                if dt_type == self.GAP:
969                    continue  # Cannot create a gap from UTC
970
971                dt_utc = (dt_naive - offset.utcoffset).replace(
972                    tzinfo=timezone.utc
973                )
974
975                # Check that we can go UTC -> Our zone
976                dt_act = dt_utc.astimezone(zi)
977                dt_exp = dt_naive.replace(tzinfo=zi)
978
979                self.assertEqual(dt_act, dt_exp)
980
981                if dt_type == self.FOLD:
982                    self.assertEqual(dt_act.fold, dt_naive.fold, dt_naive)
983                else:
984                    self.assertEqual(dt_act.fold, 0)
985
986                # Now check that we can go our zone -> UTC
987                dt_act = dt_exp.astimezone(timezone.utc)
988
989                self.assertEqual(dt_act, dt_utc)
990
991    def test_invalid_tzstr(self):
992        invalid_tzstrs = [
993            "PST8PDT",  # DST but no transition specified
994            "+11",  # Unquoted alphanumeric
995            "GMT,M3.2.0/2,M11.1.0/3",  # Transition rule but no DST
996            "GMT0+11,M3.2.0/2,M11.1.0/3",  # Unquoted alphanumeric in DST
997            "PST8PDT,M3.2.0/2",  # Only one transition rule
998            # Invalid offsets
999            "STD+25",
1000            "STD-25",
1001            "STD+374",
1002            "STD+374DST,M3.2.0/2,M11.1.0/3",
1003            "STD+23DST+25,M3.2.0/2,M11.1.0/3",
1004            "STD-23DST-25,M3.2.0/2,M11.1.0/3",
1005            # Completely invalid dates
1006            "AAA4BBB,M1443339,M11.1.0/3",
1007            "AAA4BBB,M3.2.0/2,0349309483959c",
1008            # Invalid months
1009            "AAA4BBB,M13.1.1/2,M1.1.1/2",
1010            "AAA4BBB,M1.1.1/2,M13.1.1/2",
1011            "AAA4BBB,M0.1.1/2,M1.1.1/2",
1012            "AAA4BBB,M1.1.1/2,M0.1.1/2",
1013            # Invalid weeks
1014            "AAA4BBB,M1.6.1/2,M1.1.1/2",
1015            "AAA4BBB,M1.1.1/2,M1.6.1/2",
1016            # Invalid weekday
1017            "AAA4BBB,M1.1.7/2,M2.1.1/2",
1018            "AAA4BBB,M1.1.1/2,M2.1.7/2",
1019            # Invalid numeric offset
1020            "AAA4BBB,-1/2,20/2",
1021            "AAA4BBB,1/2,-1/2",
1022            "AAA4BBB,367,20/2",
1023            "AAA4BBB,1/2,367/2",
1024            # Invalid julian offset
1025            "AAA4BBB,J0/2,J20/2",
1026            "AAA4BBB,J20/2,J366/2",
1027        ]
1028
1029        for invalid_tzstr in invalid_tzstrs:
1030            with self.subTest(tzstr=invalid_tzstr):
1031                # Not necessarily a guaranteed property, but we should show
1032                # the problematic TZ string if that's the cause of failure.
1033                tzstr_regex = re.escape(invalid_tzstr)
1034                with self.assertRaisesRegex(ValueError, tzstr_regex):
1035                    self.zone_from_tzstr(invalid_tzstr)
1036
1037    @classmethod
1038    def _populate_test_cases(cls):
1039        # This method uses a somewhat unusual style in that it populates the
1040        # test cases for each tzstr by using a decorator to automatically call
1041        # a function that mutates the current dictionary of test cases.
1042        #
1043        # The population of the test cases is done in individual functions to
1044        # give each set of test cases its own namespace in which to define
1045        # its offsets (this way we don't have to worry about variable reuse
1046        # causing problems if someone makes a typo).
1047        #
1048        # The decorator for calling is used to make it more obvious that each
1049        # function is actually called (if it's not decorated, it's not called).
1050        def call(f):
1051            """Decorator to call the addition methods.
1052
1053            This will call a function which adds at least one new entry into
1054            the `cases` dictionary. The decorator will also assert that
1055            something was added to the dictionary.
1056            """
1057            prev_len = len(cases)
1058            f()
1059            assert len(cases) > prev_len, "Function did not add a test case!"
1060
1061        NORMAL = cls.NORMAL
1062        FOLD = cls.FOLD
1063        GAP = cls.GAP
1064
1065        cases = {}
1066
1067        @call
1068        def _add():
1069            # Transition to EDT on the 2nd Sunday in March at 4 AM, and
1070            # transition back on the first Sunday in November at 3AM
1071            tzstr = "EST5EDT,M3.2.0/4:00,M11.1.0/3:00"
1072
1073            EST = ZoneOffset("EST", timedelta(hours=-5), ZERO)
1074            EDT = ZoneOffset("EDT", timedelta(hours=-4), ONE_H)
1075
1076            cases[tzstr] = (
1077                (datetime(2019, 3, 9), EST, NORMAL),
1078                (datetime(2019, 3, 10, 3, 59), EST, NORMAL),
1079                (datetime(2019, 3, 10, 4, 0, fold=0), EST, GAP),
1080                (datetime(2019, 3, 10, 4, 0, fold=1), EDT, GAP),
1081                (datetime(2019, 3, 10, 4, 1, fold=0), EST, GAP),
1082                (datetime(2019, 3, 10, 4, 1, fold=1), EDT, GAP),
1083                (datetime(2019, 11, 2), EDT, NORMAL),
1084                (datetime(2019, 11, 3, 1, 59, fold=1), EDT, NORMAL),
1085                (datetime(2019, 11, 3, 2, 0, fold=0), EDT, FOLD),
1086                (datetime(2019, 11, 3, 2, 0, fold=1), EST, FOLD),
1087                (datetime(2020, 3, 8, 3, 59), EST, NORMAL),
1088                (datetime(2020, 3, 8, 4, 0, fold=0), EST, GAP),
1089                (datetime(2020, 3, 8, 4, 0, fold=1), EDT, GAP),
1090                (datetime(2020, 11, 1, 1, 59, fold=1), EDT, NORMAL),
1091                (datetime(2020, 11, 1, 2, 0, fold=0), EDT, FOLD),
1092                (datetime(2020, 11, 1, 2, 0, fold=1), EST, FOLD),
1093            )
1094
1095        @call
1096        def _add():
1097            # Transition to BST happens on the last Sunday in March at 1 AM GMT
1098            # and the transition back happens the last Sunday in October at 2AM BST
1099            tzstr = "GMT0BST-1,M3.5.0/1:00,M10.5.0/2:00"
1100
1101            GMT = ZoneOffset("GMT", ZERO, ZERO)
1102            BST = ZoneOffset("BST", ONE_H, ONE_H)
1103
1104            cases[tzstr] = (
1105                (datetime(2019, 3, 30), GMT, NORMAL),
1106                (datetime(2019, 3, 31, 0, 59), GMT, NORMAL),
1107                (datetime(2019, 3, 31, 2, 0), BST, NORMAL),
1108                (datetime(2019, 10, 26), BST, NORMAL),
1109                (datetime(2019, 10, 27, 0, 59, fold=1), BST, NORMAL),
1110                (datetime(2019, 10, 27, 1, 0, fold=0), BST, GAP),
1111                (datetime(2019, 10, 27, 2, 0, fold=1), GMT, GAP),
1112                (datetime(2020, 3, 29, 0, 59), GMT, NORMAL),
1113                (datetime(2020, 3, 29, 2, 0), BST, NORMAL),
1114                (datetime(2020, 10, 25, 0, 59, fold=1), BST, NORMAL),
1115                (datetime(2020, 10, 25, 1, 0, fold=0), BST, FOLD),
1116                (datetime(2020, 10, 25, 2, 0, fold=1), GMT, NORMAL),
1117            )
1118
1119        @call
1120        def _add():
1121            # Austrialian time zone - DST start is chronologically first
1122            tzstr = "AEST-10AEDT,M10.1.0/2,M4.1.0/3"
1123
1124            AEST = ZoneOffset("AEST", timedelta(hours=10), ZERO)
1125            AEDT = ZoneOffset("AEDT", timedelta(hours=11), ONE_H)
1126
1127            cases[tzstr] = (
1128                (datetime(2019, 4, 6), AEDT, NORMAL),
1129                (datetime(2019, 4, 7, 1, 59), AEDT, NORMAL),
1130                (datetime(2019, 4, 7, 1, 59, fold=1), AEDT, NORMAL),
1131                (datetime(2019, 4, 7, 2, 0, fold=0), AEDT, FOLD),
1132                (datetime(2019, 4, 7, 2, 1, fold=0), AEDT, FOLD),
1133                (datetime(2019, 4, 7, 2, 0, fold=1), AEST, FOLD),
1134                (datetime(2019, 4, 7, 2, 1, fold=1), AEST, FOLD),
1135                (datetime(2019, 4, 7, 3, 0, fold=0), AEST, NORMAL),
1136                (datetime(2019, 4, 7, 3, 0, fold=1), AEST, NORMAL),
1137                (datetime(2019, 10, 5, 0), AEST, NORMAL),
1138                (datetime(2019, 10, 6, 1, 59), AEST, NORMAL),
1139                (datetime(2019, 10, 6, 2, 0, fold=0), AEST, GAP),
1140                (datetime(2019, 10, 6, 2, 0, fold=1), AEDT, GAP),
1141                (datetime(2019, 10, 6, 3, 0), AEDT, NORMAL),
1142            )
1143
1144        @call
1145        def _add():
1146            # Irish time zone - negative DST
1147            tzstr = "IST-1GMT0,M10.5.0,M3.5.0/1"
1148
1149            GMT = ZoneOffset("GMT", ZERO, -ONE_H)
1150            IST = ZoneOffset("IST", ONE_H, ZERO)
1151
1152            cases[tzstr] = (
1153                (datetime(2019, 3, 30), GMT, NORMAL),
1154                (datetime(2019, 3, 31, 0, 59), GMT, NORMAL),
1155                (datetime(2019, 3, 31, 2, 0), IST, NORMAL),
1156                (datetime(2019, 10, 26), IST, NORMAL),
1157                (datetime(2019, 10, 27, 0, 59, fold=1), IST, NORMAL),
1158                (datetime(2019, 10, 27, 1, 0, fold=0), IST, FOLD),
1159                (datetime(2019, 10, 27, 1, 0, fold=1), GMT, FOLD),
1160                (datetime(2019, 10, 27, 2, 0, fold=1), GMT, NORMAL),
1161                (datetime(2020, 3, 29, 0, 59), GMT, NORMAL),
1162                (datetime(2020, 3, 29, 2, 0), IST, NORMAL),
1163                (datetime(2020, 10, 25, 0, 59, fold=1), IST, NORMAL),
1164                (datetime(2020, 10, 25, 1, 0, fold=0), IST, FOLD),
1165                (datetime(2020, 10, 25, 2, 0, fold=1), GMT, NORMAL),
1166            )
1167
1168        @call
1169        def _add():
1170            # Pacific/Kosrae: Fixed offset zone with a quoted numerical tzname
1171            tzstr = "<+11>-11"
1172
1173            cases[tzstr] = (
1174                (
1175                    datetime(2020, 1, 1),
1176                    ZoneOffset("+11", timedelta(hours=11)),
1177                    NORMAL,
1178                ),
1179            )
1180
1181        @call
1182        def _add():
1183            # Quoted STD and DST, transitions at 24:00
1184            tzstr = "<-04>4<-03>,M9.1.6/24,M4.1.6/24"
1185
1186            M04 = ZoneOffset("-04", timedelta(hours=-4))
1187            M03 = ZoneOffset("-03", timedelta(hours=-3), ONE_H)
1188
1189            cases[tzstr] = (
1190                (datetime(2020, 5, 1), M04, NORMAL),
1191                (datetime(2020, 11, 1), M03, NORMAL),
1192            )
1193
1194        @call
1195        def _add():
1196            # Permanent daylight saving time is modeled with transitions at 0/0
1197            # and J365/25, as mentioned in RFC 8536 Section 3.3.1
1198            tzstr = "EST5EDT,0/0,J365/25"
1199
1200            EDT = ZoneOffset("EDT", timedelta(hours=-4), ONE_H)
1201
1202            cases[tzstr] = (
1203                (datetime(2019, 1, 1), EDT, NORMAL),
1204                (datetime(2019, 6, 1), EDT, NORMAL),
1205                (datetime(2019, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
1206                (datetime(2020, 1, 1), EDT, NORMAL),
1207                (datetime(2020, 3, 1), EDT, NORMAL),
1208                (datetime(2020, 6, 1), EDT, NORMAL),
1209                (datetime(2020, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
1210                (datetime(2400, 1, 1), EDT, NORMAL),
1211                (datetime(2400, 3, 1), EDT, NORMAL),
1212                (datetime(2400, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
1213            )
1214
1215        @call
1216        def _add():
1217            # Transitions on March 1st and November 1st of each year
1218            tzstr = "AAA3BBB,J60/12,J305/12"
1219
1220            AAA = ZoneOffset("AAA", timedelta(hours=-3))
1221            BBB = ZoneOffset("BBB", timedelta(hours=-2), ONE_H)
1222
1223            cases[tzstr] = (
1224                (datetime(2019, 1, 1), AAA, NORMAL),
1225                (datetime(2019, 2, 28), AAA, NORMAL),
1226                (datetime(2019, 3, 1, 11, 59), AAA, NORMAL),
1227                (datetime(2019, 3, 1, 12, fold=0), AAA, GAP),
1228                (datetime(2019, 3, 1, 12, fold=1), BBB, GAP),
1229                (datetime(2019, 3, 1, 13), BBB, NORMAL),
1230                (datetime(2019, 11, 1, 10, 59), BBB, NORMAL),
1231                (datetime(2019, 11, 1, 11, fold=0), BBB, FOLD),
1232                (datetime(2019, 11, 1, 11, fold=1), AAA, FOLD),
1233                (datetime(2019, 11, 1, 12), AAA, NORMAL),
1234                (datetime(2019, 12, 31, 23, 59, 59, 999999), AAA, NORMAL),
1235                (datetime(2020, 1, 1), AAA, NORMAL),
1236                (datetime(2020, 2, 29), AAA, NORMAL),
1237                (datetime(2020, 3, 1, 11, 59), AAA, NORMAL),
1238                (datetime(2020, 3, 1, 12, fold=0), AAA, GAP),
1239                (datetime(2020, 3, 1, 12, fold=1), BBB, GAP),
1240                (datetime(2020, 3, 1, 13), BBB, NORMAL),
1241                (datetime(2020, 11, 1, 10, 59), BBB, NORMAL),
1242                (datetime(2020, 11, 1, 11, fold=0), BBB, FOLD),
1243                (datetime(2020, 11, 1, 11, fold=1), AAA, FOLD),
1244                (datetime(2020, 11, 1, 12), AAA, NORMAL),
1245                (datetime(2020, 12, 31, 23, 59, 59, 999999), AAA, NORMAL),
1246            )
1247
1248        @call
1249        def _add():
1250            # Taken from America/Godthab, this rule has a transition on the
1251            # Saturday before the last Sunday of March and October, at 22:00
1252            # and 23:00, respectively. This is encoded with negative start
1253            # and end transition times.
1254            tzstr = "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"
1255
1256            N03 = ZoneOffset("-03", timedelta(hours=-3))
1257            N02 = ZoneOffset("-02", timedelta(hours=-2), ONE_H)
1258
1259            cases[tzstr] = (
1260                (datetime(2020, 3, 27), N03, NORMAL),
1261                (datetime(2020, 3, 28, 21, 59, 59), N03, NORMAL),
1262                (datetime(2020, 3, 28, 22, fold=0), N03, GAP),
1263                (datetime(2020, 3, 28, 22, fold=1), N02, GAP),
1264                (datetime(2020, 3, 28, 23), N02, NORMAL),
1265                (datetime(2020, 10, 24, 21), N02, NORMAL),
1266                (datetime(2020, 10, 24, 22, fold=0), N02, FOLD),
1267                (datetime(2020, 10, 24, 22, fold=1), N03, FOLD),
1268                (datetime(2020, 10, 24, 23), N03, NORMAL),
1269            )
1270
1271        @call
1272        def _add():
1273            # Transition times with minutes and seconds
1274            tzstr = "AAA3BBB,M3.2.0/01:30,M11.1.0/02:15:45"
1275
1276            AAA = ZoneOffset("AAA", timedelta(hours=-3))
1277            BBB = ZoneOffset("BBB", timedelta(hours=-2), ONE_H)
1278
1279            cases[tzstr] = (
1280                (datetime(2012, 3, 11, 1, 0), AAA, NORMAL),
1281                (datetime(2012, 3, 11, 1, 30, fold=0), AAA, GAP),
1282                (datetime(2012, 3, 11, 1, 30, fold=1), BBB, GAP),
1283                (datetime(2012, 3, 11, 2, 30), BBB, NORMAL),
1284                (datetime(2012, 11, 4, 1, 15, 44, 999999), BBB, NORMAL),
1285                (datetime(2012, 11, 4, 1, 15, 45, fold=0), BBB, FOLD),
1286                (datetime(2012, 11, 4, 1, 15, 45, fold=1), AAA, FOLD),
1287                (datetime(2012, 11, 4, 2, 15, 45), AAA, NORMAL),
1288            )
1289
1290        cls.test_cases = cases
1291
1292
1293class CTZStrTest(TZStrTest):
1294    module = c_zoneinfo
1295
1296
1297class ZoneInfoCacheTest(TzPathUserMixin, ZoneInfoTestBase):
1298    module = py_zoneinfo
1299
1300    def setUp(self):
1301        self.klass.clear_cache()
1302        super().setUp()
1303
1304    @property
1305    def zoneinfo_data(self):
1306        return ZONEINFO_DATA
1307
1308    @property
1309    def tzpath(self):
1310        return [self.zoneinfo_data.tzpath]
1311
1312    def test_ephemeral_zones(self):
1313        self.assertIs(
1314            self.klass("America/Los_Angeles"), self.klass("America/Los_Angeles")
1315        )
1316
1317    def test_strong_refs(self):
1318        tz0 = self.klass("Australia/Sydney")
1319        tz1 = self.klass("Australia/Sydney")
1320
1321        self.assertIs(tz0, tz1)
1322
1323    def test_no_cache(self):
1324
1325        tz0 = self.klass("Europe/Lisbon")
1326        tz1 = self.klass.no_cache("Europe/Lisbon")
1327
1328        self.assertIsNot(tz0, tz1)
1329
1330    def test_cache_reset_tzpath(self):
1331        """Test that the cache persists when tzpath has been changed.
1332
1333        The PEP specifies that as long as a reference exists to one zone
1334        with a given key, the primary constructor must continue to return
1335        the same object.
1336        """
1337        zi0 = self.klass("America/Los_Angeles")
1338        with self.tzpath_context([]):
1339            zi1 = self.klass("America/Los_Angeles")
1340
1341        self.assertIs(zi0, zi1)
1342
1343    def test_clear_cache_explicit_none(self):
1344        la0 = self.klass("America/Los_Angeles")
1345        self.klass.clear_cache(only_keys=None)
1346        la1 = self.klass("America/Los_Angeles")
1347
1348        self.assertIsNot(la0, la1)
1349
1350    def test_clear_cache_one_key(self):
1351        """Tests that you can clear a single key from the cache."""
1352        la0 = self.klass("America/Los_Angeles")
1353        dub0 = self.klass("Europe/Dublin")
1354
1355        self.klass.clear_cache(only_keys=["America/Los_Angeles"])
1356
1357        la1 = self.klass("America/Los_Angeles")
1358        dub1 = self.klass("Europe/Dublin")
1359
1360        self.assertIsNot(la0, la1)
1361        self.assertIs(dub0, dub1)
1362
1363    def test_clear_cache_two_keys(self):
1364        la0 = self.klass("America/Los_Angeles")
1365        dub0 = self.klass("Europe/Dublin")
1366        tok0 = self.klass("Asia/Tokyo")
1367
1368        self.klass.clear_cache(
1369            only_keys=["America/Los_Angeles", "Europe/Dublin"]
1370        )
1371
1372        la1 = self.klass("America/Los_Angeles")
1373        dub1 = self.klass("Europe/Dublin")
1374        tok1 = self.klass("Asia/Tokyo")
1375
1376        self.assertIsNot(la0, la1)
1377        self.assertIsNot(dub0, dub1)
1378        self.assertIs(tok0, tok1)
1379
1380
1381class CZoneInfoCacheTest(ZoneInfoCacheTest):
1382    module = c_zoneinfo
1383
1384
1385class ZoneInfoPickleTest(TzPathUserMixin, ZoneInfoTestBase):
1386    module = py_zoneinfo
1387
1388    def setUp(self):
1389        self.klass.clear_cache()
1390
1391        with contextlib.ExitStack() as stack:
1392            stack.enter_context(test_support.set_zoneinfo_module(self.module))
1393            self.addCleanup(stack.pop_all().close)
1394
1395        super().setUp()
1396
1397    @property
1398    def zoneinfo_data(self):
1399        return ZONEINFO_DATA
1400
1401    @property
1402    def tzpath(self):
1403        return [self.zoneinfo_data.tzpath]
1404
1405    def test_cache_hit(self):
1406        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1407            with self.subTest(proto=proto):
1408                zi_in = self.klass("Europe/Dublin")
1409                pkl = pickle.dumps(zi_in, protocol=proto)
1410                zi_rt = pickle.loads(pkl)
1411
1412                with self.subTest(test="Is non-pickled ZoneInfo"):
1413                    self.assertIs(zi_in, zi_rt)
1414
1415                zi_rt2 = pickle.loads(pkl)
1416                with self.subTest(test="Is unpickled ZoneInfo"):
1417                    self.assertIs(zi_rt, zi_rt2)
1418
1419    def test_cache_miss(self):
1420        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1421            with self.subTest(proto=proto):
1422                zi_in = self.klass("Europe/Dublin")
1423                pkl = pickle.dumps(zi_in, protocol=proto)
1424
1425                del zi_in
1426                self.klass.clear_cache()  # Induce a cache miss
1427                zi_rt = pickle.loads(pkl)
1428                zi_rt2 = pickle.loads(pkl)
1429
1430                self.assertIs(zi_rt, zi_rt2)
1431
1432    def test_no_cache(self):
1433        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1434            with self.subTest(proto=proto):
1435                zi_no_cache = self.klass.no_cache("Europe/Dublin")
1436
1437                pkl = pickle.dumps(zi_no_cache, protocol=proto)
1438                zi_rt = pickle.loads(pkl)
1439
1440                with self.subTest(test="Not the pickled object"):
1441                    self.assertIsNot(zi_rt, zi_no_cache)
1442
1443                zi_rt2 = pickle.loads(pkl)
1444                with self.subTest(test="Not a second unpickled object"):
1445                    self.assertIsNot(zi_rt, zi_rt2)
1446
1447                zi_cache = self.klass("Europe/Dublin")
1448                with self.subTest(test="Not a cached object"):
1449                    self.assertIsNot(zi_rt, zi_cache)
1450
1451    def test_from_file(self):
1452        key = "Europe/Dublin"
1453        with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
1454            zi_nokey = self.klass.from_file(f)
1455
1456            f.seek(0)
1457            zi_key = self.klass.from_file(f, key=key)
1458
1459        test_cases = [
1460            (zi_key, "ZoneInfo with key"),
1461            (zi_nokey, "ZoneInfo without key"),
1462        ]
1463
1464        for zi, test_name in test_cases:
1465            for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1466                with self.subTest(test_name=test_name, proto=proto):
1467                    with self.assertRaises(pickle.PicklingError):
1468                        pickle.dumps(zi, protocol=proto)
1469
1470    def test_pickle_after_from_file(self):
1471        # This may be a bit of paranoia, but this test is to ensure that no
1472        # global state is maintained in order to handle the pickle cache and
1473        # from_file behavior, and that it is possible to interweave the
1474        # constructors of each of these and pickling/unpickling without issues.
1475        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1476            with self.subTest(proto=proto):
1477                key = "Europe/Dublin"
1478                zi = self.klass(key)
1479
1480                pkl_0 = pickle.dumps(zi, protocol=proto)
1481                zi_rt_0 = pickle.loads(pkl_0)
1482                self.assertIs(zi, zi_rt_0)
1483
1484                with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
1485                    zi_ff = self.klass.from_file(f, key=key)
1486
1487                pkl_1 = pickle.dumps(zi, protocol=proto)
1488                zi_rt_1 = pickle.loads(pkl_1)
1489                self.assertIs(zi, zi_rt_1)
1490
1491                with self.assertRaises(pickle.PicklingError):
1492                    pickle.dumps(zi_ff, protocol=proto)
1493
1494                pkl_2 = pickle.dumps(zi, protocol=proto)
1495                zi_rt_2 = pickle.loads(pkl_2)
1496                self.assertIs(zi, zi_rt_2)
1497
1498
1499class CZoneInfoPickleTest(ZoneInfoPickleTest):
1500    module = c_zoneinfo
1501
1502
1503class CallingConventionTest(ZoneInfoTestBase):
1504    """Tests for functions with restricted calling conventions."""
1505
1506    module = py_zoneinfo
1507
1508    @property
1509    def zoneinfo_data(self):
1510        return ZONEINFO_DATA
1511
1512    def test_from_file(self):
1513        with open(self.zoneinfo_data.path_from_key("UTC"), "rb") as f:
1514            with self.assertRaises(TypeError):
1515                self.klass.from_file(fobj=f)
1516
1517    def test_clear_cache(self):
1518        with self.assertRaises(TypeError):
1519            self.klass.clear_cache(["UTC"])
1520
1521
1522class CCallingConventionTest(CallingConventionTest):
1523    module = c_zoneinfo
1524
1525
1526class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
1527    module = py_zoneinfo
1528
1529    @staticmethod
1530    @contextlib.contextmanager
1531    def python_tzpath_context(value):
1532        path_var = "PYTHONTZPATH"
1533        unset_env_sentinel = object()
1534        old_env = unset_env_sentinel
1535        try:
1536            with OS_ENV_LOCK:
1537                old_env = os.environ.get(path_var, None)
1538                os.environ[path_var] = value
1539                yield
1540        finally:
1541            if old_env is unset_env_sentinel:
1542                # In this case, `old_env` was never retrieved from the
1543                # environment for whatever reason, so there's no need to
1544                # reset the environment TZPATH.
1545                pass
1546            elif old_env is None:
1547                del os.environ[path_var]
1548            else:
1549                os.environ[path_var] = old_env  # pragma: nocover
1550
1551    def test_env_variable(self):
1552        """Tests that the environment variable works with reset_tzpath."""
1553        new_paths = [
1554            ("", []),
1555            ("/etc/zoneinfo", ["/etc/zoneinfo"]),
1556            (f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]),
1557        ]
1558
1559        for new_path_var, expected_result in new_paths:
1560            with self.python_tzpath_context(new_path_var):
1561                with self.subTest(tzpath=new_path_var):
1562                    self.module.reset_tzpath()
1563                    tzpath = self.module.TZPATH
1564                    self.assertSequenceEqual(tzpath, expected_result)
1565
1566    def test_env_variable_relative_paths(self):
1567        test_cases = [
1568            [("path/to/somewhere",), ()],
1569            [
1570                ("/usr/share/zoneinfo", "path/to/somewhere",),
1571                ("/usr/share/zoneinfo",),
1572            ],
1573            [("../relative/path",), ()],
1574            [
1575                ("/usr/share/zoneinfo", "../relative/path",),
1576                ("/usr/share/zoneinfo",),
1577            ],
1578            [("path/to/somewhere", "../relative/path",), ()],
1579            [
1580                (
1581                    "/usr/share/zoneinfo",
1582                    "path/to/somewhere",
1583                    "../relative/path",
1584                ),
1585                ("/usr/share/zoneinfo",),
1586            ],
1587        ]
1588
1589        for input_paths, expected_paths in test_cases:
1590            path_var = os.pathsep.join(input_paths)
1591            with self.python_tzpath_context(path_var):
1592                with self.subTest("warning", path_var=path_var):
1593                    # Note: Per PEP 615 the warning is implementation-defined
1594                    # behavior, other implementations need not warn.
1595                    with self.assertWarns(self.module.InvalidTZPathWarning):
1596                        self.module.reset_tzpath()
1597
1598                tzpath = self.module.TZPATH
1599                with self.subTest("filtered", path_var=path_var):
1600                    self.assertSequenceEqual(tzpath, expected_paths)
1601
1602    def test_reset_tzpath_kwarg(self):
1603        self.module.reset_tzpath(to=["/a/b/c"])
1604
1605        self.assertSequenceEqual(self.module.TZPATH, ("/a/b/c",))
1606
1607    def test_reset_tzpath_relative_paths(self):
1608        bad_values = [
1609            ("path/to/somewhere",),
1610            ("/usr/share/zoneinfo", "path/to/somewhere",),
1611            ("../relative/path",),
1612            ("/usr/share/zoneinfo", "../relative/path",),
1613            ("path/to/somewhere", "../relative/path",),
1614            ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",),
1615        ]
1616        for input_paths in bad_values:
1617            with self.subTest(input_paths=input_paths):
1618                with self.assertRaises(ValueError):
1619                    self.module.reset_tzpath(to=input_paths)
1620
1621    def test_tzpath_type_error(self):
1622        bad_values = [
1623            "/etc/zoneinfo:/usr/share/zoneinfo",
1624            b"/etc/zoneinfo:/usr/share/zoneinfo",
1625            0,
1626        ]
1627
1628        for bad_value in bad_values:
1629            with self.subTest(value=bad_value):
1630                with self.assertRaises(TypeError):
1631                    self.module.reset_tzpath(bad_value)
1632
1633    def test_tzpath_attribute(self):
1634        tzpath_0 = ["/one", "/two"]
1635        tzpath_1 = ["/three"]
1636
1637        with self.tzpath_context(tzpath_0):
1638            query_0 = self.module.TZPATH
1639
1640        with self.tzpath_context(tzpath_1):
1641            query_1 = self.module.TZPATH
1642
1643        self.assertSequenceEqual(tzpath_0, query_0)
1644        self.assertSequenceEqual(tzpath_1, query_1)
1645
1646
1647class CTzPathTest(TzPathTest):
1648    module = c_zoneinfo
1649
1650
1651class TestModule(ZoneInfoTestBase):
1652    module = py_zoneinfo
1653
1654    @property
1655    def zoneinfo_data(self):
1656        return ZONEINFO_DATA
1657
1658    @cached_property
1659    def _UTC_bytes(self):
1660        zone_file = self.zoneinfo_data.path_from_key("UTC")
1661        with open(zone_file, "rb") as f:
1662            return f.read()
1663
1664    def touch_zone(self, key, tz_root):
1665        """Creates a valid TZif file at key under the zoneinfo root tz_root.
1666
1667        tz_root must exist, but all folders below that will be created.
1668        """
1669        if not os.path.exists(tz_root):
1670            raise FileNotFoundError(f"{tz_root} does not exist.")
1671
1672        root_dir, *tail = key.rsplit("/", 1)
1673        if tail:  # If there's no tail, then the first component isn't a dir
1674            os.makedirs(os.path.join(tz_root, root_dir), exist_ok=True)
1675
1676        zonefile_path = os.path.join(tz_root, key)
1677        with open(zonefile_path, "wb") as f:
1678            f.write(self._UTC_bytes)
1679
1680    def test_getattr_error(self):
1681        with self.assertRaises(AttributeError):
1682            self.module.NOATTRIBUTE
1683
1684    def test_dir_contains_all(self):
1685        """dir(self.module) should at least contain everything in __all__."""
1686        module_all_set = set(self.module.__all__)
1687        module_dir_set = set(dir(self.module))
1688
1689        difference = module_all_set - module_dir_set
1690
1691        self.assertFalse(difference)
1692
1693    def test_dir_unique(self):
1694        """Test that there are no duplicates in dir(self.module)"""
1695        module_dir = dir(self.module)
1696        module_unique = set(module_dir)
1697
1698        self.assertCountEqual(module_dir, module_unique)
1699
1700    def test_available_timezones(self):
1701        with self.tzpath_context([self.zoneinfo_data.tzpath]):
1702            self.assertTrue(self.zoneinfo_data.keys)  # Sanity check
1703
1704            available_keys = self.module.available_timezones()
1705            zoneinfo_keys = set(self.zoneinfo_data.keys)
1706
1707            # If tzdata is not present, zoneinfo_keys == available_keys,
1708            # otherwise it should be a subset.
1709            union = zoneinfo_keys & available_keys
1710            self.assertEqual(zoneinfo_keys, union)
1711
1712    def test_available_timezones_weirdzone(self):
1713        with tempfile.TemporaryDirectory() as td:
1714            # Make a fictional zone at "Mars/Olympus_Mons"
1715            self.touch_zone("Mars/Olympus_Mons", td)
1716
1717            with self.tzpath_context([td]):
1718                available_keys = self.module.available_timezones()
1719                self.assertIn("Mars/Olympus_Mons", available_keys)
1720
1721    def test_folder_exclusions(self):
1722        expected = {
1723            "America/Los_Angeles",
1724            "America/Santiago",
1725            "America/Indiana/Indianapolis",
1726            "UTC",
1727            "Europe/Paris",
1728            "Europe/London",
1729            "Asia/Tokyo",
1730            "Australia/Sydney",
1731        }
1732
1733        base_tree = list(expected)
1734        posix_tree = [f"posix/{x}" for x in base_tree]
1735        right_tree = [f"right/{x}" for x in base_tree]
1736
1737        cases = [
1738            ("base_tree", base_tree),
1739            ("base_and_posix", base_tree + posix_tree),
1740            ("base_and_right", base_tree + right_tree),
1741            ("all_trees", base_tree + right_tree + posix_tree),
1742        ]
1743
1744        with tempfile.TemporaryDirectory() as td:
1745            for case_name, tree in cases:
1746                tz_root = os.path.join(td, case_name)
1747                os.mkdir(tz_root)
1748
1749                for key in tree:
1750                    self.touch_zone(key, tz_root)
1751
1752                with self.tzpath_context([tz_root]):
1753                    with self.subTest(case_name):
1754                        actual = self.module.available_timezones()
1755                        self.assertEqual(actual, expected)
1756
1757    def test_exclude_posixrules(self):
1758        expected = {
1759            "America/New_York",
1760            "Europe/London",
1761        }
1762
1763        tree = list(expected) + ["posixrules"]
1764
1765        with tempfile.TemporaryDirectory() as td:
1766            for key in tree:
1767                self.touch_zone(key, td)
1768
1769            with self.tzpath_context([td]):
1770                actual = self.module.available_timezones()
1771                self.assertEqual(actual, expected)
1772
1773
1774class CTestModule(TestModule):
1775    module = c_zoneinfo
1776
1777
1778class ExtensionBuiltTest(unittest.TestCase):
1779    """Smoke test to ensure that the C and Python extensions are both tested.
1780
1781    Because the intention is for the Python and C versions of ZoneInfo to
1782    behave identically, these tests necessarily rely on implementation details,
1783    so the tests may need to be adjusted if the implementations change. Do not
1784    rely on these tests as an indication of stable properties of these classes.
1785    """
1786
1787    def test_cache_location(self):
1788        # The pure Python version stores caches on attributes, but the C
1789        # extension stores them in C globals (at least for now)
1790        self.assertFalse(hasattr(c_zoneinfo.ZoneInfo, "_weak_cache"))
1791        self.assertTrue(hasattr(py_zoneinfo.ZoneInfo, "_weak_cache"))
1792
1793    def test_gc_tracked(self):
1794        # The pure Python version is tracked by the GC but (for now) the C
1795        # version is not.
1796        import gc
1797
1798        self.assertTrue(gc.is_tracked(py_zoneinfo.ZoneInfo))
1799        self.assertFalse(gc.is_tracked(c_zoneinfo.ZoneInfo))
1800
1801
1802@dataclasses.dataclass(frozen=True)
1803class ZoneOffset:
1804    tzname: str
1805    utcoffset: timedelta
1806    dst: timedelta = ZERO
1807
1808
1809@dataclasses.dataclass(frozen=True)
1810class ZoneTransition:
1811    transition: datetime
1812    offset_before: ZoneOffset
1813    offset_after: ZoneOffset
1814
1815    @property
1816    def transition_utc(self):
1817        return (self.transition - self.offset_before.utcoffset).replace(
1818            tzinfo=timezone.utc
1819        )
1820
1821    @property
1822    def fold(self):
1823        """Whether this introduces a fold"""
1824        return self.offset_before.utcoffset > self.offset_after.utcoffset
1825
1826    @property
1827    def gap(self):
1828        """Whether this introduces a gap"""
1829        return self.offset_before.utcoffset < self.offset_after.utcoffset
1830
1831    @property
1832    def delta(self):
1833        return self.offset_after.utcoffset - self.offset_before.utcoffset
1834
1835    @property
1836    def anomaly_start(self):
1837        if self.fold:
1838            return self.transition + self.delta
1839        else:
1840            return self.transition
1841
1842    @property
1843    def anomaly_end(self):
1844        if not self.fold:
1845            return self.transition + self.delta
1846        else:
1847            return self.transition
1848
1849
1850class ZoneInfoData:
1851    def __init__(self, source_json, tzpath, v1=False):
1852        self.tzpath = pathlib.Path(tzpath)
1853        self.keys = []
1854        self.v1 = v1
1855        self._populate_tzpath(source_json)
1856
1857    def path_from_key(self, key):
1858        return self.tzpath / key
1859
1860    def _populate_tzpath(self, source_json):
1861        with open(source_json, "rb") as f:
1862            zoneinfo_dict = json.load(f)
1863
1864        zoneinfo_data = zoneinfo_dict["data"]
1865
1866        for key, value in zoneinfo_data.items():
1867            self.keys.append(key)
1868            raw_data = self._decode_text(value)
1869
1870            if self.v1:
1871                data = self._convert_to_v1(raw_data)
1872            else:
1873                data = raw_data
1874
1875            destination = self.path_from_key(key)
1876            destination.parent.mkdir(exist_ok=True, parents=True)
1877            with open(destination, "wb") as f:
1878                f.write(data)
1879
1880    def _decode_text(self, contents):
1881        raw_data = b"".join(map(str.encode, contents))
1882        decoded = base64.b85decode(raw_data)
1883
1884        return lzma.decompress(decoded)
1885
1886    def _convert_to_v1(self, contents):
1887        assert contents[0:4] == b"TZif", "Invalid TZif data found!"
1888        version = int(contents[4:5])
1889
1890        header_start = 4 + 16
1891        header_end = header_start + 24  # 6l == 24 bytes
1892        assert version >= 2, "Version 1 file found: no conversion necessary"
1893        isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt = struct.unpack(
1894            ">6l", contents[header_start:header_end]
1895        )
1896
1897        file_size = (
1898            timecnt * 5
1899            + typecnt * 6
1900            + charcnt
1901            + leapcnt * 8
1902            + isstdcnt
1903            + isutcnt
1904        )
1905        file_size += header_end
1906        out = b"TZif" + b"\x00" + contents[5:file_size]
1907
1908        assert (
1909            contents[file_size : (file_size + 4)] == b"TZif"
1910        ), "Version 2 file not truncated at Version 2 header"
1911
1912        return out
1913
1914
1915class ZoneDumpData:
1916    @classmethod
1917    def transition_keys(cls):
1918        return cls._get_zonedump().keys()
1919
1920    @classmethod
1921    def load_transition_examples(cls, key):
1922        return cls._get_zonedump()[key]
1923
1924    @classmethod
1925    def fixed_offset_zones(cls):
1926        if not cls._FIXED_OFFSET_ZONES:
1927            cls._populate_fixed_offsets()
1928
1929        return cls._FIXED_OFFSET_ZONES.items()
1930
1931    @classmethod
1932    def _get_zonedump(cls):
1933        if not cls._ZONEDUMP_DATA:
1934            cls._populate_zonedump_data()
1935        return cls._ZONEDUMP_DATA
1936
1937    @classmethod
1938    def _populate_fixed_offsets(cls):
1939        cls._FIXED_OFFSET_ZONES = {
1940            "UTC": ZoneOffset("UTC", ZERO, ZERO),
1941        }
1942
1943    @classmethod
1944    def _populate_zonedump_data(cls):
1945        def _Africa_Abidjan():
1946            LMT = ZoneOffset("LMT", timedelta(seconds=-968))
1947            GMT = ZoneOffset("GMT", ZERO)
1948
1949            return [
1950                ZoneTransition(datetime(1912, 1, 1), LMT, GMT),
1951            ]
1952
1953        def _Africa_Casablanca():
1954            P00_s = ZoneOffset("+00", ZERO, ZERO)
1955            P01_d = ZoneOffset("+01", ONE_H, ONE_H)
1956            P00_d = ZoneOffset("+00", ZERO, -ONE_H)
1957            P01_s = ZoneOffset("+01", ONE_H, ZERO)
1958
1959            return [
1960                # Morocco sometimes pauses DST during Ramadan
1961                ZoneTransition(datetime(2018, 3, 25, 2), P00_s, P01_d),
1962                ZoneTransition(datetime(2018, 5, 13, 3), P01_d, P00_s),
1963                ZoneTransition(datetime(2018, 6, 17, 2), P00_s, P01_d),
1964                # On October 28th Morocco set standard time to +01,
1965                # with negative DST only during Ramadan
1966                ZoneTransition(datetime(2018, 10, 28, 3), P01_d, P01_s),
1967                ZoneTransition(datetime(2019, 5, 5, 3), P01_s, P00_d),
1968                ZoneTransition(datetime(2019, 6, 9, 2), P00_d, P01_s),
1969            ]
1970
1971        def _America_Los_Angeles():
1972            LMT = ZoneOffset("LMT", timedelta(seconds=-28378), ZERO)
1973            PST = ZoneOffset("PST", timedelta(hours=-8), ZERO)
1974            PDT = ZoneOffset("PDT", timedelta(hours=-7), ONE_H)
1975            PWT = ZoneOffset("PWT", timedelta(hours=-7), ONE_H)
1976            PPT = ZoneOffset("PPT", timedelta(hours=-7), ONE_H)
1977
1978            return [
1979                ZoneTransition(datetime(1883, 11, 18, 12, 7, 2), LMT, PST),
1980                ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
1981                ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
1982                ZoneTransition(datetime(1918, 10, 27, 2), PDT, PST),
1983                # Transition to Pacific War Time
1984                ZoneTransition(datetime(1942, 2, 9, 2), PST, PWT),
1985                # Transition from Pacific War Time to Pacific Peace Time
1986                ZoneTransition(datetime(1945, 8, 14, 16), PWT, PPT),
1987                ZoneTransition(datetime(1945, 9, 30, 2), PPT, PST),
1988                ZoneTransition(datetime(2015, 3, 8, 2), PST, PDT),
1989                ZoneTransition(datetime(2015, 11, 1, 2), PDT, PST),
1990                # After 2038: Rules continue indefinitely
1991                ZoneTransition(datetime(2450, 3, 13, 2), PST, PDT),
1992                ZoneTransition(datetime(2450, 11, 6, 2), PDT, PST),
1993            ]
1994
1995        def _America_Santiago():
1996            LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO)
1997            SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO)
1998            N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO)
1999            N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO)
2000            N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H)
2001
2002            return [
2003                ZoneTransition(datetime(1890, 1, 1), LMT, SMT),
2004                ZoneTransition(datetime(1910, 1, 10), SMT, N05),
2005                ZoneTransition(datetime(1916, 7, 1), N05, SMT),
2006                ZoneTransition(datetime(2008, 3, 30), N03, N04),
2007                ZoneTransition(datetime(2008, 10, 12), N04, N03),
2008                ZoneTransition(datetime(2040, 4, 8), N03, N04),
2009                ZoneTransition(datetime(2040, 9, 2), N04, N03),
2010            ]
2011
2012        def _Asia_Tokyo():
2013            JST = ZoneOffset("JST", timedelta(seconds=32400), ZERO)
2014            JDT = ZoneOffset("JDT", timedelta(seconds=36000), ONE_H)
2015
2016            # Japan had DST from 1948 to 1951, and it was unusual in that
2017            # the transition from DST to STD occurred at 25:00, and is
2018            # denominated as such in the time zone database
2019            return [
2020                ZoneTransition(datetime(1948, 5, 2), JST, JDT),
2021                ZoneTransition(datetime(1948, 9, 12, 1), JDT, JST),
2022                ZoneTransition(datetime(1951, 9, 9, 1), JDT, JST),
2023            ]
2024
2025        def _Australia_Sydney():
2026            LMT = ZoneOffset("LMT", timedelta(seconds=36292), ZERO)
2027            AEST = ZoneOffset("AEST", timedelta(seconds=36000), ZERO)
2028            AEDT = ZoneOffset("AEDT", timedelta(seconds=39600), ONE_H)
2029
2030            return [
2031                ZoneTransition(datetime(1895, 2, 1), LMT, AEST),
2032                ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT),
2033                ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST),
2034                ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST),
2035                ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT),
2036                ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST),
2037                ZoneTransition(datetime(2040, 10, 7, 2), AEST, AEDT),
2038            ]
2039
2040        def _Europe_Dublin():
2041            LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO)
2042            DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO)
2043            IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H)
2044            GMT_0 = ZoneOffset("GMT", ZERO, ZERO)
2045            BST = ZoneOffset("BST", ONE_H, ONE_H)
2046            GMT_1 = ZoneOffset("GMT", ZERO, -ONE_H)
2047            IST_1 = ZoneOffset("IST", ONE_H, ZERO)
2048
2049            return [
2050                ZoneTransition(datetime(1880, 8, 2, 0), LMT, DMT),
2051                ZoneTransition(datetime(1916, 5, 21, 2), DMT, IST_0),
2052                ZoneTransition(datetime(1916, 10, 1, 3), IST_0, GMT_0),
2053                ZoneTransition(datetime(1917, 4, 8, 2), GMT_0, BST),
2054                ZoneTransition(datetime(2016, 3, 27, 1), GMT_1, IST_1),
2055                ZoneTransition(datetime(2016, 10, 30, 2), IST_1, GMT_1),
2056                ZoneTransition(datetime(2487, 3, 30, 1), GMT_1, IST_1),
2057                ZoneTransition(datetime(2487, 10, 26, 2), IST_1, GMT_1),
2058            ]
2059
2060        def _Europe_Lisbon():
2061            WET = ZoneOffset("WET", ZERO, ZERO)
2062            WEST = ZoneOffset("WEST", ONE_H, ONE_H)
2063            CET = ZoneOffset("CET", ONE_H, ZERO)
2064            CEST = ZoneOffset("CEST", timedelta(seconds=7200), ONE_H)
2065
2066            return [
2067                ZoneTransition(datetime(1992, 3, 29, 1), WET, WEST),
2068                ZoneTransition(datetime(1992, 9, 27, 2), WEST, CET),
2069                ZoneTransition(datetime(1993, 3, 28, 2), CET, CEST),
2070                ZoneTransition(datetime(1993, 9, 26, 3), CEST, CET),
2071                ZoneTransition(datetime(1996, 3, 31, 2), CET, WEST),
2072                ZoneTransition(datetime(1996, 10, 27, 2), WEST, WET),
2073            ]
2074
2075        def _Europe_London():
2076            LMT = ZoneOffset("LMT", timedelta(seconds=-75), ZERO)
2077            GMT = ZoneOffset("GMT", ZERO, ZERO)
2078            BST = ZoneOffset("BST", ONE_H, ONE_H)
2079
2080            return [
2081                ZoneTransition(datetime(1847, 12, 1), LMT, GMT),
2082                ZoneTransition(datetime(2005, 3, 27, 1), GMT, BST),
2083                ZoneTransition(datetime(2005, 10, 30, 2), BST, GMT),
2084                ZoneTransition(datetime(2043, 3, 29, 1), GMT, BST),
2085                ZoneTransition(datetime(2043, 10, 25, 2), BST, GMT),
2086            ]
2087
2088        def _Pacific_Kiritimati():
2089            LMT = ZoneOffset("LMT", timedelta(seconds=-37760), ZERO)
2090            N1040 = ZoneOffset("-1040", timedelta(seconds=-38400), ZERO)
2091            N10 = ZoneOffset("-10", timedelta(seconds=-36000), ZERO)
2092            P14 = ZoneOffset("+14", timedelta(seconds=50400), ZERO)
2093
2094            # This is literally every transition in Christmas Island history
2095            return [
2096                ZoneTransition(datetime(1901, 1, 1), LMT, N1040),
2097                ZoneTransition(datetime(1979, 10, 1), N1040, N10),
2098                # They skipped December 31, 1994
2099                ZoneTransition(datetime(1994, 12, 31), N10, P14),
2100            ]
2101
2102        cls._ZONEDUMP_DATA = {
2103            "Africa/Abidjan": _Africa_Abidjan(),
2104            "Africa/Casablanca": _Africa_Casablanca(),
2105            "America/Los_Angeles": _America_Los_Angeles(),
2106            "America/Santiago": _America_Santiago(),
2107            "Australia/Sydney": _Australia_Sydney(),
2108            "Asia/Tokyo": _Asia_Tokyo(),
2109            "Europe/Dublin": _Europe_Dublin(),
2110            "Europe/Lisbon": _Europe_Lisbon(),
2111            "Europe/London": _Europe_London(),
2112            "Pacific/Kiritimati": _Pacific_Kiritimati(),
2113        }
2114
2115    _ZONEDUMP_DATA = None
2116    _FIXED_OFFSET_ZONES = None
2117
2118
2119if __name__ == '__main__':
2120    unittest.main()
2121