• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 . import _support as test_support
21from ._support import OS_ENV_LOCK, TZPATH_TEST_LOCK, ZoneInfoTestBase
22from test.support 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        zi_in = self.klass("Europe/Dublin")
1407        pkl = pickle.dumps(zi_in)
1408        zi_rt = pickle.loads(pkl)
1409
1410        with self.subTest(test="Is non-pickled ZoneInfo"):
1411            self.assertIs(zi_in, zi_rt)
1412
1413        zi_rt2 = pickle.loads(pkl)
1414        with self.subTest(test="Is unpickled ZoneInfo"):
1415            self.assertIs(zi_rt, zi_rt2)
1416
1417    def test_cache_miss(self):
1418        zi_in = self.klass("Europe/Dublin")
1419        pkl = pickle.dumps(zi_in)
1420
1421        del zi_in
1422        self.klass.clear_cache()  # Induce a cache miss
1423        zi_rt = pickle.loads(pkl)
1424        zi_rt2 = pickle.loads(pkl)
1425
1426        self.assertIs(zi_rt, zi_rt2)
1427
1428    def test_no_cache(self):
1429        zi_no_cache = self.klass.no_cache("Europe/Dublin")
1430
1431        pkl = pickle.dumps(zi_no_cache)
1432        zi_rt = pickle.loads(pkl)
1433
1434        with self.subTest(test="Not the pickled object"):
1435            self.assertIsNot(zi_rt, zi_no_cache)
1436
1437        zi_rt2 = pickle.loads(pkl)
1438        with self.subTest(test="Not a second unpickled object"):
1439            self.assertIsNot(zi_rt, zi_rt2)
1440
1441        zi_cache = self.klass("Europe/Dublin")
1442        with self.subTest(test="Not a cached object"):
1443            self.assertIsNot(zi_rt, zi_cache)
1444
1445    def test_from_file(self):
1446        key = "Europe/Dublin"
1447        with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
1448            zi_nokey = self.klass.from_file(f)
1449
1450            f.seek(0)
1451            zi_key = self.klass.from_file(f, key=key)
1452
1453        test_cases = [
1454            (zi_key, "ZoneInfo with key"),
1455            (zi_nokey, "ZoneInfo without key"),
1456        ]
1457
1458        for zi, test_name in test_cases:
1459            with self.subTest(test_name=test_name):
1460                with self.assertRaises(pickle.PicklingError):
1461                    pickle.dumps(zi)
1462
1463    def test_pickle_after_from_file(self):
1464        # This may be a bit of paranoia, but this test is to ensure that no
1465        # global state is maintained in order to handle the pickle cache and
1466        # from_file behavior, and that it is possible to interweave the
1467        # constructors of each of these and pickling/unpickling without issues.
1468        key = "Europe/Dublin"
1469        zi = self.klass(key)
1470
1471        pkl_0 = pickle.dumps(zi)
1472        zi_rt_0 = pickle.loads(pkl_0)
1473        self.assertIs(zi, zi_rt_0)
1474
1475        with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
1476            zi_ff = self.klass.from_file(f, key=key)
1477
1478        pkl_1 = pickle.dumps(zi)
1479        zi_rt_1 = pickle.loads(pkl_1)
1480        self.assertIs(zi, zi_rt_1)
1481
1482        with self.assertRaises(pickle.PicklingError):
1483            pickle.dumps(zi_ff)
1484
1485        pkl_2 = pickle.dumps(zi)
1486        zi_rt_2 = pickle.loads(pkl_2)
1487        self.assertIs(zi, zi_rt_2)
1488
1489
1490class CZoneInfoPickleTest(ZoneInfoPickleTest):
1491    module = c_zoneinfo
1492
1493
1494class CallingConventionTest(ZoneInfoTestBase):
1495    """Tests for functions with restricted calling conventions."""
1496
1497    module = py_zoneinfo
1498
1499    @property
1500    def zoneinfo_data(self):
1501        return ZONEINFO_DATA
1502
1503    def test_from_file(self):
1504        with open(self.zoneinfo_data.path_from_key("UTC"), "rb") as f:
1505            with self.assertRaises(TypeError):
1506                self.klass.from_file(fobj=f)
1507
1508    def test_clear_cache(self):
1509        with self.assertRaises(TypeError):
1510            self.klass.clear_cache(["UTC"])
1511
1512
1513class CCallingConventionTest(CallingConventionTest):
1514    module = c_zoneinfo
1515
1516
1517class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
1518    module = py_zoneinfo
1519
1520    @staticmethod
1521    @contextlib.contextmanager
1522    def python_tzpath_context(value):
1523        path_var = "PYTHONTZPATH"
1524        try:
1525            with OS_ENV_LOCK:
1526                old_env = os.environ.get(path_var, None)
1527                os.environ[path_var] = value
1528                yield
1529        finally:
1530            if old_env is None:
1531                del os.environ[path_var]
1532            else:
1533                os.environ[path_var] = old_env  # pragma: nocover
1534
1535    def test_env_variable(self):
1536        """Tests that the environment variable works with reset_tzpath."""
1537        new_paths = [
1538            ("", []),
1539            ("/etc/zoneinfo", ["/etc/zoneinfo"]),
1540            (f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]),
1541        ]
1542
1543        for new_path_var, expected_result in new_paths:
1544            with self.python_tzpath_context(new_path_var):
1545                with self.subTest(tzpath=new_path_var):
1546                    self.module.reset_tzpath()
1547                    tzpath = self.module.TZPATH
1548                    self.assertSequenceEqual(tzpath, expected_result)
1549
1550    def test_env_variable_relative_paths(self):
1551        test_cases = [
1552            [("path/to/somewhere",), ()],
1553            [
1554                ("/usr/share/zoneinfo", "path/to/somewhere",),
1555                ("/usr/share/zoneinfo",),
1556            ],
1557            [("../relative/path",), ()],
1558            [
1559                ("/usr/share/zoneinfo", "../relative/path",),
1560                ("/usr/share/zoneinfo",),
1561            ],
1562            [("path/to/somewhere", "../relative/path",), ()],
1563            [
1564                (
1565                    "/usr/share/zoneinfo",
1566                    "path/to/somewhere",
1567                    "../relative/path",
1568                ),
1569                ("/usr/share/zoneinfo",),
1570            ],
1571        ]
1572
1573        for input_paths, expected_paths in test_cases:
1574            path_var = os.pathsep.join(input_paths)
1575            with self.python_tzpath_context(path_var):
1576                with self.subTest("warning", path_var=path_var):
1577                    # Note: Per PEP 615 the warning is implementation-defined
1578                    # behavior, other implementations need not warn.
1579                    with self.assertWarns(self.module.InvalidTZPathWarning):
1580                        self.module.reset_tzpath()
1581
1582                tzpath = self.module.TZPATH
1583                with self.subTest("filtered", path_var=path_var):
1584                    self.assertSequenceEqual(tzpath, expected_paths)
1585
1586    def test_reset_tzpath_kwarg(self):
1587        self.module.reset_tzpath(to=["/a/b/c"])
1588
1589        self.assertSequenceEqual(self.module.TZPATH, ("/a/b/c",))
1590
1591    def test_reset_tzpath_relative_paths(self):
1592        bad_values = [
1593            ("path/to/somewhere",),
1594            ("/usr/share/zoneinfo", "path/to/somewhere",),
1595            ("../relative/path",),
1596            ("/usr/share/zoneinfo", "../relative/path",),
1597            ("path/to/somewhere", "../relative/path",),
1598            ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",),
1599        ]
1600        for input_paths in bad_values:
1601            with self.subTest(input_paths=input_paths):
1602                with self.assertRaises(ValueError):
1603                    self.module.reset_tzpath(to=input_paths)
1604
1605    def test_tzpath_type_error(self):
1606        bad_values = [
1607            "/etc/zoneinfo:/usr/share/zoneinfo",
1608            b"/etc/zoneinfo:/usr/share/zoneinfo",
1609            0,
1610        ]
1611
1612        for bad_value in bad_values:
1613            with self.subTest(value=bad_value):
1614                with self.assertRaises(TypeError):
1615                    self.module.reset_tzpath(bad_value)
1616
1617    def test_tzpath_attribute(self):
1618        tzpath_0 = ["/one", "/two"]
1619        tzpath_1 = ["/three"]
1620
1621        with self.tzpath_context(tzpath_0):
1622            query_0 = self.module.TZPATH
1623
1624        with self.tzpath_context(tzpath_1):
1625            query_1 = self.module.TZPATH
1626
1627        self.assertSequenceEqual(tzpath_0, query_0)
1628        self.assertSequenceEqual(tzpath_1, query_1)
1629
1630
1631class CTzPathTest(TzPathTest):
1632    module = c_zoneinfo
1633
1634
1635class TestModule(ZoneInfoTestBase):
1636    module = py_zoneinfo
1637
1638    @property
1639    def zoneinfo_data(self):
1640        return ZONEINFO_DATA
1641
1642    @cached_property
1643    def _UTC_bytes(self):
1644        zone_file = self.zoneinfo_data.path_from_key("UTC")
1645        with open(zone_file, "rb") as f:
1646            return f.read()
1647
1648    def touch_zone(self, key, tz_root):
1649        """Creates a valid TZif file at key under the zoneinfo root tz_root.
1650
1651        tz_root must exist, but all folders below that will be created.
1652        """
1653        if not os.path.exists(tz_root):
1654            raise FileNotFoundError(f"{tz_root} does not exist.")
1655
1656        root_dir, *tail = key.rsplit("/", 1)
1657        if tail:  # If there's no tail, then the first component isn't a dir
1658            os.makedirs(os.path.join(tz_root, root_dir), exist_ok=True)
1659
1660        zonefile_path = os.path.join(tz_root, key)
1661        with open(zonefile_path, "wb") as f:
1662            f.write(self._UTC_bytes)
1663
1664    def test_getattr_error(self):
1665        with self.assertRaises(AttributeError):
1666            self.module.NOATTRIBUTE
1667
1668    def test_dir_contains_all(self):
1669        """dir(self.module) should at least contain everything in __all__."""
1670        module_all_set = set(self.module.__all__)
1671        module_dir_set = set(dir(self.module))
1672
1673        difference = module_all_set - module_dir_set
1674
1675        self.assertFalse(difference)
1676
1677    def test_dir_unique(self):
1678        """Test that there are no duplicates in dir(self.module)"""
1679        module_dir = dir(self.module)
1680        module_unique = set(module_dir)
1681
1682        self.assertCountEqual(module_dir, module_unique)
1683
1684    def test_available_timezones(self):
1685        with self.tzpath_context([self.zoneinfo_data.tzpath]):
1686            self.assertTrue(self.zoneinfo_data.keys)  # Sanity check
1687
1688            available_keys = self.module.available_timezones()
1689            zoneinfo_keys = set(self.zoneinfo_data.keys)
1690
1691            # If tzdata is not present, zoneinfo_keys == available_keys,
1692            # otherwise it should be a subset.
1693            union = zoneinfo_keys & available_keys
1694            self.assertEqual(zoneinfo_keys, union)
1695
1696    def test_available_timezones_weirdzone(self):
1697        with tempfile.TemporaryDirectory() as td:
1698            # Make a fictional zone at "Mars/Olympus_Mons"
1699            self.touch_zone("Mars/Olympus_Mons", td)
1700
1701            with self.tzpath_context([td]):
1702                available_keys = self.module.available_timezones()
1703                self.assertIn("Mars/Olympus_Mons", available_keys)
1704
1705    def test_folder_exclusions(self):
1706        expected = {
1707            "America/Los_Angeles",
1708            "America/Santiago",
1709            "America/Indiana/Indianapolis",
1710            "UTC",
1711            "Europe/Paris",
1712            "Europe/London",
1713            "Asia/Tokyo",
1714            "Australia/Sydney",
1715        }
1716
1717        base_tree = list(expected)
1718        posix_tree = [f"posix/{x}" for x in base_tree]
1719        right_tree = [f"right/{x}" for x in base_tree]
1720
1721        cases = [
1722            ("base_tree", base_tree),
1723            ("base_and_posix", base_tree + posix_tree),
1724            ("base_and_right", base_tree + right_tree),
1725            ("all_trees", base_tree + right_tree + posix_tree),
1726        ]
1727
1728        with tempfile.TemporaryDirectory() as td:
1729            for case_name, tree in cases:
1730                tz_root = os.path.join(td, case_name)
1731                os.mkdir(tz_root)
1732
1733                for key in tree:
1734                    self.touch_zone(key, tz_root)
1735
1736                with self.tzpath_context([tz_root]):
1737                    with self.subTest(case_name):
1738                        actual = self.module.available_timezones()
1739                        self.assertEqual(actual, expected)
1740
1741    def test_exclude_posixrules(self):
1742        expected = {
1743            "America/New_York",
1744            "Europe/London",
1745        }
1746
1747        tree = list(expected) + ["posixrules"]
1748
1749        with tempfile.TemporaryDirectory() as td:
1750            for key in tree:
1751                self.touch_zone(key, td)
1752
1753            with self.tzpath_context([td]):
1754                actual = self.module.available_timezones()
1755                self.assertEqual(actual, expected)
1756
1757
1758class CTestModule(TestModule):
1759    module = c_zoneinfo
1760
1761
1762class ExtensionBuiltTest(unittest.TestCase):
1763    """Smoke test to ensure that the C and Python extensions are both tested.
1764
1765    Because the intention is for the Python and C versions of ZoneInfo to
1766    behave identically, these tests necessarily rely on implementation details,
1767    so the tests may need to be adjusted if the implementations change. Do not
1768    rely on these tests as an indication of stable properties of these classes.
1769    """
1770
1771    def test_cache_location(self):
1772        # The pure Python version stores caches on attributes, but the C
1773        # extension stores them in C globals (at least for now)
1774        self.assertFalse(hasattr(c_zoneinfo.ZoneInfo, "_weak_cache"))
1775        self.assertTrue(hasattr(py_zoneinfo.ZoneInfo, "_weak_cache"))
1776
1777    def test_gc_tracked(self):
1778        # The pure Python version is tracked by the GC but (for now) the C
1779        # version is not.
1780        import gc
1781
1782        self.assertTrue(gc.is_tracked(py_zoneinfo.ZoneInfo))
1783        self.assertFalse(gc.is_tracked(c_zoneinfo.ZoneInfo))
1784
1785
1786@dataclasses.dataclass(frozen=True)
1787class ZoneOffset:
1788    tzname: str
1789    utcoffset: timedelta
1790    dst: timedelta = ZERO
1791
1792
1793@dataclasses.dataclass(frozen=True)
1794class ZoneTransition:
1795    transition: datetime
1796    offset_before: ZoneOffset
1797    offset_after: ZoneOffset
1798
1799    @property
1800    def transition_utc(self):
1801        return (self.transition - self.offset_before.utcoffset).replace(
1802            tzinfo=timezone.utc
1803        )
1804
1805    @property
1806    def fold(self):
1807        """Whether this introduces a fold"""
1808        return self.offset_before.utcoffset > self.offset_after.utcoffset
1809
1810    @property
1811    def gap(self):
1812        """Whether this introduces a gap"""
1813        return self.offset_before.utcoffset < self.offset_after.utcoffset
1814
1815    @property
1816    def delta(self):
1817        return self.offset_after.utcoffset - self.offset_before.utcoffset
1818
1819    @property
1820    def anomaly_start(self):
1821        if self.fold:
1822            return self.transition + self.delta
1823        else:
1824            return self.transition
1825
1826    @property
1827    def anomaly_end(self):
1828        if not self.fold:
1829            return self.transition + self.delta
1830        else:
1831            return self.transition
1832
1833
1834class ZoneInfoData:
1835    def __init__(self, source_json, tzpath, v1=False):
1836        self.tzpath = pathlib.Path(tzpath)
1837        self.keys = []
1838        self.v1 = v1
1839        self._populate_tzpath(source_json)
1840
1841    def path_from_key(self, key):
1842        return self.tzpath / key
1843
1844    def _populate_tzpath(self, source_json):
1845        with open(source_json, "rb") as f:
1846            zoneinfo_dict = json.load(f)
1847
1848        zoneinfo_data = zoneinfo_dict["data"]
1849
1850        for key, value in zoneinfo_data.items():
1851            self.keys.append(key)
1852            raw_data = self._decode_text(value)
1853
1854            if self.v1:
1855                data = self._convert_to_v1(raw_data)
1856            else:
1857                data = raw_data
1858
1859            destination = self.path_from_key(key)
1860            destination.parent.mkdir(exist_ok=True, parents=True)
1861            with open(destination, "wb") as f:
1862                f.write(data)
1863
1864    def _decode_text(self, contents):
1865        raw_data = b"".join(map(str.encode, contents))
1866        decoded = base64.b85decode(raw_data)
1867
1868        return lzma.decompress(decoded)
1869
1870    def _convert_to_v1(self, contents):
1871        assert contents[0:4] == b"TZif", "Invalid TZif data found!"
1872        version = int(contents[4:5])
1873
1874        header_start = 4 + 16
1875        header_end = header_start + 24  # 6l == 24 bytes
1876        assert version >= 2, "Version 1 file found: no conversion necessary"
1877        isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt = struct.unpack(
1878            ">6l", contents[header_start:header_end]
1879        )
1880
1881        file_size = (
1882            timecnt * 5
1883            + typecnt * 6
1884            + charcnt
1885            + leapcnt * 8
1886            + isstdcnt
1887            + isutcnt
1888        )
1889        file_size += header_end
1890        out = b"TZif" + b"\x00" + contents[5:file_size]
1891
1892        assert (
1893            contents[file_size : (file_size + 4)] == b"TZif"
1894        ), "Version 2 file not truncated at Version 2 header"
1895
1896        return out
1897
1898
1899class ZoneDumpData:
1900    @classmethod
1901    def transition_keys(cls):
1902        return cls._get_zonedump().keys()
1903
1904    @classmethod
1905    def load_transition_examples(cls, key):
1906        return cls._get_zonedump()[key]
1907
1908    @classmethod
1909    def fixed_offset_zones(cls):
1910        if not cls._FIXED_OFFSET_ZONES:
1911            cls._populate_fixed_offsets()
1912
1913        return cls._FIXED_OFFSET_ZONES.items()
1914
1915    @classmethod
1916    def _get_zonedump(cls):
1917        if not cls._ZONEDUMP_DATA:
1918            cls._populate_zonedump_data()
1919        return cls._ZONEDUMP_DATA
1920
1921    @classmethod
1922    def _populate_fixed_offsets(cls):
1923        cls._FIXED_OFFSET_ZONES = {
1924            "UTC": ZoneOffset("UTC", ZERO, ZERO),
1925        }
1926
1927    @classmethod
1928    def _populate_zonedump_data(cls):
1929        def _Africa_Abidjan():
1930            LMT = ZoneOffset("LMT", timedelta(seconds=-968))
1931            GMT = ZoneOffset("GMT", ZERO)
1932
1933            return [
1934                ZoneTransition(datetime(1912, 1, 1), LMT, GMT),
1935            ]
1936
1937        def _Africa_Casablanca():
1938            P00_s = ZoneOffset("+00", ZERO, ZERO)
1939            P01_d = ZoneOffset("+01", ONE_H, ONE_H)
1940            P00_d = ZoneOffset("+00", ZERO, -ONE_H)
1941            P01_s = ZoneOffset("+01", ONE_H, ZERO)
1942
1943            return [
1944                # Morocco sometimes pauses DST during Ramadan
1945                ZoneTransition(datetime(2018, 3, 25, 2), P00_s, P01_d),
1946                ZoneTransition(datetime(2018, 5, 13, 3), P01_d, P00_s),
1947                ZoneTransition(datetime(2018, 6, 17, 2), P00_s, P01_d),
1948                # On October 28th Morocco set standard time to +01,
1949                # with negative DST only during Ramadan
1950                ZoneTransition(datetime(2018, 10, 28, 3), P01_d, P01_s),
1951                ZoneTransition(datetime(2019, 5, 5, 3), P01_s, P00_d),
1952                ZoneTransition(datetime(2019, 6, 9, 2), P00_d, P01_s),
1953            ]
1954
1955        def _America_Los_Angeles():
1956            LMT = ZoneOffset("LMT", timedelta(seconds=-28378), ZERO)
1957            PST = ZoneOffset("PST", timedelta(hours=-8), ZERO)
1958            PDT = ZoneOffset("PDT", timedelta(hours=-7), ONE_H)
1959            PWT = ZoneOffset("PWT", timedelta(hours=-7), ONE_H)
1960            PPT = ZoneOffset("PPT", timedelta(hours=-7), ONE_H)
1961
1962            return [
1963                ZoneTransition(datetime(1883, 11, 18, 12, 7, 2), LMT, PST),
1964                ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
1965                ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
1966                ZoneTransition(datetime(1918, 10, 27, 2), PDT, PST),
1967                # Transition to Pacific War Time
1968                ZoneTransition(datetime(1942, 2, 9, 2), PST, PWT),
1969                # Transition from Pacific War Time to Pacific Peace Time
1970                ZoneTransition(datetime(1945, 8, 14, 16), PWT, PPT),
1971                ZoneTransition(datetime(1945, 9, 30, 2), PPT, PST),
1972                ZoneTransition(datetime(2015, 3, 8, 2), PST, PDT),
1973                ZoneTransition(datetime(2015, 11, 1, 2), PDT, PST),
1974                # After 2038: Rules continue indefinitely
1975                ZoneTransition(datetime(2450, 3, 13, 2), PST, PDT),
1976                ZoneTransition(datetime(2450, 11, 6, 2), PDT, PST),
1977            ]
1978
1979        def _America_Santiago():
1980            LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO)
1981            SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO)
1982            N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO)
1983            N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO)
1984            N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H)
1985
1986            return [
1987                ZoneTransition(datetime(1890, 1, 1), LMT, SMT),
1988                ZoneTransition(datetime(1910, 1, 10), SMT, N05),
1989                ZoneTransition(datetime(1916, 7, 1), N05, SMT),
1990                ZoneTransition(datetime(2008, 3, 30), N03, N04),
1991                ZoneTransition(datetime(2008, 10, 12), N04, N03),
1992                ZoneTransition(datetime(2040, 4, 8), N03, N04),
1993                ZoneTransition(datetime(2040, 9, 2), N04, N03),
1994            ]
1995
1996        def _Asia_Tokyo():
1997            JST = ZoneOffset("JST", timedelta(seconds=32400), ZERO)
1998            JDT = ZoneOffset("JDT", timedelta(seconds=36000), ONE_H)
1999
2000            # Japan had DST from 1948 to 1951, and it was unusual in that
2001            # the transition from DST to STD occurred at 25:00, and is
2002            # denominated as such in the time zone database
2003            return [
2004                ZoneTransition(datetime(1948, 5, 2), JST, JDT),
2005                ZoneTransition(datetime(1948, 9, 12, 1), JDT, JST),
2006                ZoneTransition(datetime(1951, 9, 9, 1), JDT, JST),
2007            ]
2008
2009        def _Australia_Sydney():
2010            LMT = ZoneOffset("LMT", timedelta(seconds=36292), ZERO)
2011            AEST = ZoneOffset("AEST", timedelta(seconds=36000), ZERO)
2012            AEDT = ZoneOffset("AEDT", timedelta(seconds=39600), ONE_H)
2013
2014            return [
2015                ZoneTransition(datetime(1895, 2, 1), LMT, AEST),
2016                ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT),
2017                ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST),
2018                ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST),
2019                ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT),
2020                ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST),
2021                ZoneTransition(datetime(2040, 10, 7, 2), AEST, AEDT),
2022            ]
2023
2024        def _Europe_Dublin():
2025            LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO)
2026            DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO)
2027            IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H)
2028            GMT_0 = ZoneOffset("GMT", ZERO, ZERO)
2029            BST = ZoneOffset("BST", ONE_H, ONE_H)
2030            GMT_1 = ZoneOffset("GMT", ZERO, -ONE_H)
2031            IST_1 = ZoneOffset("IST", ONE_H, ZERO)
2032
2033            return [
2034                ZoneTransition(datetime(1880, 8, 2, 0), LMT, DMT),
2035                ZoneTransition(datetime(1916, 5, 21, 2), DMT, IST_0),
2036                ZoneTransition(datetime(1916, 10, 1, 3), IST_0, GMT_0),
2037                ZoneTransition(datetime(1917, 4, 8, 2), GMT_0, BST),
2038                ZoneTransition(datetime(2016, 3, 27, 1), GMT_1, IST_1),
2039                ZoneTransition(datetime(2016, 10, 30, 2), IST_1, GMT_1),
2040                ZoneTransition(datetime(2487, 3, 30, 1), GMT_1, IST_1),
2041                ZoneTransition(datetime(2487, 10, 26, 2), IST_1, GMT_1),
2042            ]
2043
2044        def _Europe_Lisbon():
2045            WET = ZoneOffset("WET", ZERO, ZERO)
2046            WEST = ZoneOffset("WEST", ONE_H, ONE_H)
2047            CET = ZoneOffset("CET", ONE_H, ZERO)
2048            CEST = ZoneOffset("CEST", timedelta(seconds=7200), ONE_H)
2049
2050            return [
2051                ZoneTransition(datetime(1992, 3, 29, 1), WET, WEST),
2052                ZoneTransition(datetime(1992, 9, 27, 2), WEST, CET),
2053                ZoneTransition(datetime(1993, 3, 28, 2), CET, CEST),
2054                ZoneTransition(datetime(1993, 9, 26, 3), CEST, CET),
2055                ZoneTransition(datetime(1996, 3, 31, 2), CET, WEST),
2056                ZoneTransition(datetime(1996, 10, 27, 2), WEST, WET),
2057            ]
2058
2059        def _Europe_London():
2060            LMT = ZoneOffset("LMT", timedelta(seconds=-75), ZERO)
2061            GMT = ZoneOffset("GMT", ZERO, ZERO)
2062            BST = ZoneOffset("BST", ONE_H, ONE_H)
2063
2064            return [
2065                ZoneTransition(datetime(1847, 12, 1), LMT, GMT),
2066                ZoneTransition(datetime(2005, 3, 27, 1), GMT, BST),
2067                ZoneTransition(datetime(2005, 10, 30, 2), BST, GMT),
2068                ZoneTransition(datetime(2043, 3, 29, 1), GMT, BST),
2069                ZoneTransition(datetime(2043, 10, 25, 2), BST, GMT),
2070            ]
2071
2072        def _Pacific_Kiritimati():
2073            LMT = ZoneOffset("LMT", timedelta(seconds=-37760), ZERO)
2074            N1040 = ZoneOffset("-1040", timedelta(seconds=-38400), ZERO)
2075            N10 = ZoneOffset("-10", timedelta(seconds=-36000), ZERO)
2076            P14 = ZoneOffset("+14", timedelta(seconds=50400), ZERO)
2077
2078            # This is literally every transition in Christmas Island history
2079            return [
2080                ZoneTransition(datetime(1901, 1, 1), LMT, N1040),
2081                ZoneTransition(datetime(1979, 10, 1), N1040, N10),
2082                # They skipped December 31, 1994
2083                ZoneTransition(datetime(1994, 12, 31), N10, P14),
2084            ]
2085
2086        cls._ZONEDUMP_DATA = {
2087            "Africa/Abidjan": _Africa_Abidjan(),
2088            "Africa/Casablanca": _Africa_Casablanca(),
2089            "America/Los_Angeles": _America_Los_Angeles(),
2090            "America/Santiago": _America_Santiago(),
2091            "Australia/Sydney": _Australia_Sydney(),
2092            "Asia/Tokyo": _Asia_Tokyo(),
2093            "Europe/Dublin": _Europe_Dublin(),
2094            "Europe/Lisbon": _Europe_Lisbon(),
2095            "Europe/London": _Europe_London(),
2096            "Pacific/Kiritimati": _Pacific_Kiritimati(),
2097        }
2098
2099    _ZONEDUMP_DATA = None
2100    _FIXED_OFFSET_ZONES = None
2101