python (3.11.7)
       1  from __future__ import annotations
       2  
       3  import base64
       4  import contextlib
       5  import dataclasses
       6  import importlib.metadata
       7  import io
       8  import json
       9  import os
      10  import pathlib
      11  import pickle
      12  import re
      13  import shutil
      14  import struct
      15  import tempfile
      16  import unittest
      17  from datetime import date, datetime, time, timedelta, timezone
      18  from functools import cached_property
      19  
      20  from test.test_zoneinfo import _support as test_support
      21  from test.test_zoneinfo._support import OS_ENV_LOCK, TZPATH_TEST_LOCK, ZoneInfoTestBase
      22  from test.support.import_helper import import_module
      23  
      24  lzma = import_module('lzma')
      25  py_zoneinfo, c_zoneinfo = test_support.get_modules()
      26  
      27  try:
      28      importlib.metadata.metadata("tzdata")
      29      HAS_TZDATA_PKG = True
      30  except importlib.metadata.PackageNotFoundError:
      31      HAS_TZDATA_PKG = False
      32  
      33  ZONEINFO_DATA = None
      34  ZONEINFO_DATA_V1 = None
      35  TEMP_DIR = None
      36  DATA_DIR = pathlib.Path(__file__).parent / "data"
      37  ZONEINFO_JSON = DATA_DIR / "zoneinfo_data.json"
      38  
      39  # Useful constants
      40  ZERO = timedelta(0)
      41  ONE_H = timedelta(hours=1)
      42  
      43  
      44  def 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  
      54  def tearDownModule():
      55      shutil.rmtree(TEMP_DIR)
      56  
      57  
      58  class ESC[4;38;5;81mTzPathUserMixin:
      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  
      90  class ESC[4;38;5;81mDatetimeSubclassMixin:
      91      """
      92      Replaces all ZoneTransition transition dates with a datetime subclass.
      93      """
      94  
      95      class ESC[4;38;5;81mDatetimeSubclass(ESC[4;38;5;149mdatetime):
      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  
     119  class ESC[4;38;5;81mZoneInfoTest(ESC[4;38;5;149mTzPathUserMixin, ESC[4;38;5;149mZoneInfoTestBase):
     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  
     404  class ESC[4;38;5;81mCZoneInfoTest(ESC[4;38;5;149mZoneInfoTest):
     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 ESC[4;38;5;81mSameAddSubclass(ESC[4;38;5;149mtype(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  
     458  class ESC[4;38;5;81mZoneInfoDatetimeSubclassTest(ESC[4;38;5;149mDatetimeSubclassMixin, ESC[4;38;5;149mZoneInfoTest):
     459      pass
     460  
     461  
     462  class ESC[4;38;5;81mCZoneInfoDatetimeSubclassTest(ESC[4;38;5;149mDatetimeSubclassMixin, ESC[4;38;5;149mCZoneInfoTest):
     463      pass
     464  
     465  
     466  class ESC[4;38;5;81mZoneInfoSubclassTest(ESC[4;38;5;149mZoneInfoTest):
     467      @classmethod
     468      def setUpClass(cls):
     469          super().setUpClass()
     470  
     471          class ESC[4;38;5;81mZISubclass(ESC[4;38;5;149mclsESC[4;38;5;149m.ESC[4;38;5;149mklass):
     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  
     487  class ESC[4;38;5;81mCZoneInfoSubclassTest(ESC[4;38;5;149mZoneInfoSubclassTest):
     488      module = c_zoneinfo
     489  
     490  
     491  class ESC[4;38;5;81mZoneInfoV1Test(ESC[4;38;5;149mZoneInfoTest):
     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  
     509  class ESC[4;38;5;81mCZoneInfoV1Test(ESC[4;38;5;149mZoneInfoV1Test):
     510      module = c_zoneinfo
     511  
     512  
     513  @unittest.skipIf(
     514      not HAS_TZDATA_PKG, "Skipping tzdata-specific tests: tzdata not installed"
     515  )
     516  class ESC[4;38;5;81mTZDataTests(ESC[4;38;5;149mZoneInfoTest):
     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  )
     540  class ESC[4;38;5;81mCTZDataTests(ESC[4;38;5;149mTZDataTests):
     541      module = c_zoneinfo
     542  
     543  
     544  class ESC[4;38;5;81mWeirdZoneTest(ESC[4;38;5;149mZoneInfoTestBase):
     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  
     900  class ESC[4;38;5;81mCWeirdZoneTest(ESC[4;38;5;149mWeirdZoneTest):
     901      module = c_zoneinfo
     902  
     903  
     904  class ESC[4;38;5;81mTZStrTest(ESC[4;38;5;149mZoneInfoTestBase):
     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_extreme_tzstr(self):
     992          tzstrs = [
     993              # Extreme offset hour
     994              "AAA24",
     995              "AAA+24",
     996              "AAA-24",
     997              "AAA24BBB,J60/2,J300/2",
     998              "AAA+24BBB,J60/2,J300/2",
     999              "AAA-24BBB,J60/2,J300/2",
    1000              "AAA4BBB24,J60/2,J300/2",
    1001              "AAA4BBB+24,J60/2,J300/2",
    1002              "AAA4BBB-24,J60/2,J300/2",
    1003              # Extreme offset minutes
    1004              "AAA4:00BBB,J60/2,J300/2",
    1005              "AAA4:59BBB,J60/2,J300/2",
    1006              "AAA4BBB5:00,J60/2,J300/2",
    1007              "AAA4BBB5:59,J60/2,J300/2",
    1008              # Extreme offset seconds
    1009              "AAA4:00:00BBB,J60/2,J300/2",
    1010              "AAA4:00:59BBB,J60/2,J300/2",
    1011              "AAA4BBB5:00:00,J60/2,J300/2",
    1012              "AAA4BBB5:00:59,J60/2,J300/2",
    1013              # Extreme total offset
    1014              "AAA24:59:59BBB5,J60/2,J300/2",
    1015              "AAA-24:59:59BBB5,J60/2,J300/2",
    1016              "AAA4BBB24:59:59,J60/2,J300/2",
    1017              "AAA4BBB-24:59:59,J60/2,J300/2",
    1018              # Extreme months
    1019              "AAA4BBB,M12.1.1/2,M1.1.1/2",
    1020              "AAA4BBB,M1.1.1/2,M12.1.1/2",
    1021              # Extreme weeks
    1022              "AAA4BBB,M1.5.1/2,M1.1.1/2",
    1023              "AAA4BBB,M1.1.1/2,M1.5.1/2",
    1024              # Extreme weekday
    1025              "AAA4BBB,M1.1.6/2,M2.1.1/2",
    1026              "AAA4BBB,M1.1.1/2,M2.1.6/2",
    1027              # Extreme numeric offset
    1028              "AAA4BBB,0/2,20/2",
    1029              "AAA4BBB,0/2,0/14",
    1030              "AAA4BBB,20/2,365/2",
    1031              "AAA4BBB,365/2,365/14",
    1032              # Extreme julian offset
    1033              "AAA4BBB,J1/2,J20/2",
    1034              "AAA4BBB,J1/2,J1/14",
    1035              "AAA4BBB,J20/2,J365/2",
    1036              "AAA4BBB,J365/2,J365/14",
    1037              # Extreme transition hour
    1038              "AAA4BBB,J60/167,J300/2",
    1039              "AAA4BBB,J60/+167,J300/2",
    1040              "AAA4BBB,J60/-167,J300/2",
    1041              "AAA4BBB,J60/2,J300/167",
    1042              "AAA4BBB,J60/2,J300/+167",
    1043              "AAA4BBB,J60/2,J300/-167",
    1044              # Extreme transition minutes
    1045              "AAA4BBB,J60/2:00,J300/2",
    1046              "AAA4BBB,J60/2:59,J300/2",
    1047              "AAA4BBB,J60/2,J300/2:00",
    1048              "AAA4BBB,J60/2,J300/2:59",
    1049              # Extreme transition seconds
    1050              "AAA4BBB,J60/2:00:00,J300/2",
    1051              "AAA4BBB,J60/2:00:59,J300/2",
    1052              "AAA4BBB,J60/2,J300/2:00:00",
    1053              "AAA4BBB,J60/2,J300/2:00:59",
    1054              # Extreme total transition time
    1055              "AAA4BBB,J60/167:59:59,J300/2",
    1056              "AAA4BBB,J60/-167:59:59,J300/2",
    1057              "AAA4BBB,J60/2,J300/167:59:59",
    1058              "AAA4BBB,J60/2,J300/-167:59:59",
    1059          ]
    1060  
    1061          for tzstr in tzstrs:
    1062              with self.subTest(tzstr=tzstr):
    1063                  self.zone_from_tzstr(tzstr)
    1064  
    1065      def test_invalid_tzstr(self):
    1066          invalid_tzstrs = [
    1067              "PST8PDT",  # DST but no transition specified
    1068              "+11",  # Unquoted alphanumeric
    1069              "GMT,M3.2.0/2,M11.1.0/3",  # Transition rule but no DST
    1070              "GMT0+11,M3.2.0/2,M11.1.0/3",  # Unquoted alphanumeric in DST
    1071              "PST8PDT,M3.2.0/2",  # Only one transition rule
    1072              # Invalid offset hours
    1073              "AAA168",
    1074              "AAA+168",
    1075              "AAA-168",
    1076              "AAA168BBB,J60/2,J300/2",
    1077              "AAA+168BBB,J60/2,J300/2",
    1078              "AAA-168BBB,J60/2,J300/2",
    1079              "AAA4BBB168,J60/2,J300/2",
    1080              "AAA4BBB+168,J60/2,J300/2",
    1081              "AAA4BBB-168,J60/2,J300/2",
    1082              # Invalid offset minutes
    1083              "AAA4:0BBB,J60/2,J300/2",
    1084              "AAA4:100BBB,J60/2,J300/2",
    1085              "AAA4BBB5:0,J60/2,J300/2",
    1086              "AAA4BBB5:100,J60/2,J300/2",
    1087              # Invalid offset seconds
    1088              "AAA4:00:0BBB,J60/2,J300/2",
    1089              "AAA4:00:100BBB,J60/2,J300/2",
    1090              "AAA4BBB5:00:0,J60/2,J300/2",
    1091              "AAA4BBB5:00:100,J60/2,J300/2",
    1092              # Completely invalid dates
    1093              "AAA4BBB,M1443339,M11.1.0/3",
    1094              "AAA4BBB,M3.2.0/2,0349309483959c",
    1095              "AAA4BBB,,J300/2",
    1096              "AAA4BBB,z,J300/2",
    1097              "AAA4BBB,J60/2,",
    1098              "AAA4BBB,J60/2,z",
    1099              # Invalid months
    1100              "AAA4BBB,M13.1.1/2,M1.1.1/2",
    1101              "AAA4BBB,M1.1.1/2,M13.1.1/2",
    1102              "AAA4BBB,M0.1.1/2,M1.1.1/2",
    1103              "AAA4BBB,M1.1.1/2,M0.1.1/2",
    1104              # Invalid weeks
    1105              "AAA4BBB,M1.6.1/2,M1.1.1/2",
    1106              "AAA4BBB,M1.1.1/2,M1.6.1/2",
    1107              # Invalid weekday
    1108              "AAA4BBB,M1.1.7/2,M2.1.1/2",
    1109              "AAA4BBB,M1.1.1/2,M2.1.7/2",
    1110              # Invalid numeric offset
    1111              "AAA4BBB,-1/2,20/2",
    1112              "AAA4BBB,1/2,-1/2",
    1113              "AAA4BBB,367,20/2",
    1114              "AAA4BBB,1/2,367/2",
    1115              # Invalid julian offset
    1116              "AAA4BBB,J0/2,J20/2",
    1117              "AAA4BBB,J20/2,J366/2",
    1118              # Invalid transition time
    1119              "AAA4BBB,J60/2/3,J300/2",
    1120              "AAA4BBB,J60/2,J300/2/3",
    1121              # Invalid transition hour
    1122              "AAA4BBB,J60/168,J300/2",
    1123              "AAA4BBB,J60/+168,J300/2",
    1124              "AAA4BBB,J60/-168,J300/2",
    1125              "AAA4BBB,J60/2,J300/168",
    1126              "AAA4BBB,J60/2,J300/+168",
    1127              "AAA4BBB,J60/2,J300/-168",
    1128              # Invalid transition minutes
    1129              "AAA4BBB,J60/2:0,J300/2",
    1130              "AAA4BBB,J60/2:100,J300/2",
    1131              "AAA4BBB,J60/2,J300/2:0",
    1132              "AAA4BBB,J60/2,J300/2:100",
    1133              # Invalid transition seconds
    1134              "AAA4BBB,J60/2:00:0,J300/2",
    1135              "AAA4BBB,J60/2:00:100,J300/2",
    1136              "AAA4BBB,J60/2,J300/2:00:0",
    1137              "AAA4BBB,J60/2,J300/2:00:100",
    1138          ]
    1139  
    1140          for invalid_tzstr in invalid_tzstrs:
    1141              with self.subTest(tzstr=invalid_tzstr):
    1142                  # Not necessarily a guaranteed property, but we should show
    1143                  # the problematic TZ string if that's the cause of failure.
    1144                  tzstr_regex = re.escape(invalid_tzstr)
    1145                  with self.assertRaisesRegex(ValueError, tzstr_regex):
    1146                      self.zone_from_tzstr(invalid_tzstr)
    1147  
    1148      @classmethod
    1149      def _populate_test_cases(cls):
    1150          # This method uses a somewhat unusual style in that it populates the
    1151          # test cases for each tzstr by using a decorator to automatically call
    1152          # a function that mutates the current dictionary of test cases.
    1153          #
    1154          # The population of the test cases is done in individual functions to
    1155          # give each set of test cases its own namespace in which to define
    1156          # its offsets (this way we don't have to worry about variable reuse
    1157          # causing problems if someone makes a typo).
    1158          #
    1159          # The decorator for calling is used to make it more obvious that each
    1160          # function is actually called (if it's not decorated, it's not called).
    1161          def call(f):
    1162              """Decorator to call the addition methods.
    1163  
    1164              This will call a function which adds at least one new entry into
    1165              the `cases` dictionary. The decorator will also assert that
    1166              something was added to the dictionary.
    1167              """
    1168              prev_len = len(cases)
    1169              f()
    1170              assert len(cases) > prev_len, "Function did not add a test case!"
    1171  
    1172          NORMAL = cls.NORMAL
    1173          FOLD = cls.FOLD
    1174          GAP = cls.GAP
    1175  
    1176          cases = {}
    1177  
    1178          @call
    1179          def _add():
    1180              # Transition to EDT on the 2nd Sunday in March at 4 AM, and
    1181              # transition back on the first Sunday in November at 3AM
    1182              tzstr = "EST5EDT,M3.2.0/4:00,M11.1.0/3:00"
    1183  
    1184              EST = ZoneOffset("EST", timedelta(hours=-5), ZERO)
    1185              EDT = ZoneOffset("EDT", timedelta(hours=-4), ONE_H)
    1186  
    1187              cases[tzstr] = (
    1188                  (datetime(2019, 3, 9), EST, NORMAL),
    1189                  (datetime(2019, 3, 10, 3, 59), EST, NORMAL),
    1190                  (datetime(2019, 3, 10, 4, 0, fold=0), EST, GAP),
    1191                  (datetime(2019, 3, 10, 4, 0, fold=1), EDT, GAP),
    1192                  (datetime(2019, 3, 10, 4, 1, fold=0), EST, GAP),
    1193                  (datetime(2019, 3, 10, 4, 1, fold=1), EDT, GAP),
    1194                  (datetime(2019, 11, 2), EDT, NORMAL),
    1195                  (datetime(2019, 11, 3, 1, 59, fold=1), EDT, NORMAL),
    1196                  (datetime(2019, 11, 3, 2, 0, fold=0), EDT, FOLD),
    1197                  (datetime(2019, 11, 3, 2, 0, fold=1), EST, FOLD),
    1198                  (datetime(2020, 3, 8, 3, 59), EST, NORMAL),
    1199                  (datetime(2020, 3, 8, 4, 0, fold=0), EST, GAP),
    1200                  (datetime(2020, 3, 8, 4, 0, fold=1), EDT, GAP),
    1201                  (datetime(2020, 11, 1, 1, 59, fold=1), EDT, NORMAL),
    1202                  (datetime(2020, 11, 1, 2, 0, fold=0), EDT, FOLD),
    1203                  (datetime(2020, 11, 1, 2, 0, fold=1), EST, FOLD),
    1204              )
    1205  
    1206          @call
    1207          def _add():
    1208              # Transition to BST happens on the last Sunday in March at 1 AM GMT
    1209              # and the transition back happens the last Sunday in October at 2AM BST
    1210              tzstr = "GMT0BST-1,M3.5.0/1:00,M10.5.0/2:00"
    1211  
    1212              GMT = ZoneOffset("GMT", ZERO, ZERO)
    1213              BST = ZoneOffset("BST", ONE_H, ONE_H)
    1214  
    1215              cases[tzstr] = (
    1216                  (datetime(2019, 3, 30), GMT, NORMAL),
    1217                  (datetime(2019, 3, 31, 0, 59), GMT, NORMAL),
    1218                  (datetime(2019, 3, 31, 2, 0), BST, NORMAL),
    1219                  (datetime(2019, 10, 26), BST, NORMAL),
    1220                  (datetime(2019, 10, 27, 0, 59, fold=1), BST, NORMAL),
    1221                  (datetime(2019, 10, 27, 1, 0, fold=0), BST, GAP),
    1222                  (datetime(2019, 10, 27, 2, 0, fold=1), GMT, GAP),
    1223                  (datetime(2020, 3, 29, 0, 59), GMT, NORMAL),
    1224                  (datetime(2020, 3, 29, 2, 0), BST, NORMAL),
    1225                  (datetime(2020, 10, 25, 0, 59, fold=1), BST, NORMAL),
    1226                  (datetime(2020, 10, 25, 1, 0, fold=0), BST, FOLD),
    1227                  (datetime(2020, 10, 25, 2, 0, fold=1), GMT, NORMAL),
    1228              )
    1229  
    1230          @call
    1231          def _add():
    1232              # Austrialian time zone - DST start is chronologically first
    1233              tzstr = "AEST-10AEDT,M10.1.0/2,M4.1.0/3"
    1234  
    1235              AEST = ZoneOffset("AEST", timedelta(hours=10), ZERO)
    1236              AEDT = ZoneOffset("AEDT", timedelta(hours=11), ONE_H)
    1237  
    1238              cases[tzstr] = (
    1239                  (datetime(2019, 4, 6), AEDT, NORMAL),
    1240                  (datetime(2019, 4, 7, 1, 59), AEDT, NORMAL),
    1241                  (datetime(2019, 4, 7, 1, 59, fold=1), AEDT, NORMAL),
    1242                  (datetime(2019, 4, 7, 2, 0, fold=0), AEDT, FOLD),
    1243                  (datetime(2019, 4, 7, 2, 1, fold=0), AEDT, FOLD),
    1244                  (datetime(2019, 4, 7, 2, 0, fold=1), AEST, FOLD),
    1245                  (datetime(2019, 4, 7, 2, 1, fold=1), AEST, FOLD),
    1246                  (datetime(2019, 4, 7, 3, 0, fold=0), AEST, NORMAL),
    1247                  (datetime(2019, 4, 7, 3, 0, fold=1), AEST, NORMAL),
    1248                  (datetime(2019, 10, 5, 0), AEST, NORMAL),
    1249                  (datetime(2019, 10, 6, 1, 59), AEST, NORMAL),
    1250                  (datetime(2019, 10, 6, 2, 0, fold=0), AEST, GAP),
    1251                  (datetime(2019, 10, 6, 2, 0, fold=1), AEDT, GAP),
    1252                  (datetime(2019, 10, 6, 3, 0), AEDT, NORMAL),
    1253              )
    1254  
    1255          @call
    1256          def _add():
    1257              # Irish time zone - negative DST
    1258              tzstr = "IST-1GMT0,M10.5.0,M3.5.0/1"
    1259  
    1260              GMT = ZoneOffset("GMT", ZERO, -ONE_H)
    1261              IST = ZoneOffset("IST", ONE_H, ZERO)
    1262  
    1263              cases[tzstr] = (
    1264                  (datetime(2019, 3, 30), GMT, NORMAL),
    1265                  (datetime(2019, 3, 31, 0, 59), GMT, NORMAL),
    1266                  (datetime(2019, 3, 31, 2, 0), IST, NORMAL),
    1267                  (datetime(2019, 10, 26), IST, NORMAL),
    1268                  (datetime(2019, 10, 27, 0, 59, fold=1), IST, NORMAL),
    1269                  (datetime(2019, 10, 27, 1, 0, fold=0), IST, FOLD),
    1270                  (datetime(2019, 10, 27, 1, 0, fold=1), GMT, FOLD),
    1271                  (datetime(2019, 10, 27, 2, 0, fold=1), GMT, NORMAL),
    1272                  (datetime(2020, 3, 29, 0, 59), GMT, NORMAL),
    1273                  (datetime(2020, 3, 29, 2, 0), IST, NORMAL),
    1274                  (datetime(2020, 10, 25, 0, 59, fold=1), IST, NORMAL),
    1275                  (datetime(2020, 10, 25, 1, 0, fold=0), IST, FOLD),
    1276                  (datetime(2020, 10, 25, 2, 0, fold=1), GMT, NORMAL),
    1277              )
    1278  
    1279          @call
    1280          def _add():
    1281              # Pacific/Kosrae: Fixed offset zone with a quoted numerical tzname
    1282              tzstr = "<+11>-11"
    1283  
    1284              cases[tzstr] = (
    1285                  (
    1286                      datetime(2020, 1, 1),
    1287                      ZoneOffset("+11", timedelta(hours=11)),
    1288                      NORMAL,
    1289                  ),
    1290              )
    1291  
    1292          @call
    1293          def _add():
    1294              # Quoted STD and DST, transitions at 24:00
    1295              tzstr = "<-04>4<-03>,M9.1.6/24,M4.1.6/24"
    1296  
    1297              M04 = ZoneOffset("-04", timedelta(hours=-4))
    1298              M03 = ZoneOffset("-03", timedelta(hours=-3), ONE_H)
    1299  
    1300              cases[tzstr] = (
    1301                  (datetime(2020, 5, 1), M04, NORMAL),
    1302                  (datetime(2020, 11, 1), M03, NORMAL),
    1303              )
    1304  
    1305          @call
    1306          def _add():
    1307              # Permanent daylight saving time is modeled with transitions at 0/0
    1308              # and J365/25, as mentioned in RFC 8536 Section 3.3.1
    1309              tzstr = "EST5EDT,0/0,J365/25"
    1310  
    1311              EDT = ZoneOffset("EDT", timedelta(hours=-4), ONE_H)
    1312  
    1313              cases[tzstr] = (
    1314                  (datetime(2019, 1, 1), EDT, NORMAL),
    1315                  (datetime(2019, 6, 1), EDT, NORMAL),
    1316                  (datetime(2019, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
    1317                  (datetime(2020, 1, 1), EDT, NORMAL),
    1318                  (datetime(2020, 3, 1), EDT, NORMAL),
    1319                  (datetime(2020, 6, 1), EDT, NORMAL),
    1320                  (datetime(2020, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
    1321                  (datetime(2400, 1, 1), EDT, NORMAL),
    1322                  (datetime(2400, 3, 1), EDT, NORMAL),
    1323                  (datetime(2400, 12, 31, 23, 59, 59, 999999), EDT, NORMAL),
    1324              )
    1325  
    1326          @call
    1327          def _add():
    1328              # Transitions on March 1st and November 1st of each year
    1329              tzstr = "AAA3BBB,J60/12,J305/12"
    1330  
    1331              AAA = ZoneOffset("AAA", timedelta(hours=-3))
    1332              BBB = ZoneOffset("BBB", timedelta(hours=-2), ONE_H)
    1333  
    1334              cases[tzstr] = (
    1335                  (datetime(2019, 1, 1), AAA, NORMAL),
    1336                  (datetime(2019, 2, 28), AAA, NORMAL),
    1337                  (datetime(2019, 3, 1, 11, 59), AAA, NORMAL),
    1338                  (datetime(2019, 3, 1, 12, fold=0), AAA, GAP),
    1339                  (datetime(2019, 3, 1, 12, fold=1), BBB, GAP),
    1340                  (datetime(2019, 3, 1, 13), BBB, NORMAL),
    1341                  (datetime(2019, 11, 1, 10, 59), BBB, NORMAL),
    1342                  (datetime(2019, 11, 1, 11, fold=0), BBB, FOLD),
    1343                  (datetime(2019, 11, 1, 11, fold=1), AAA, FOLD),
    1344                  (datetime(2019, 11, 1, 12), AAA, NORMAL),
    1345                  (datetime(2019, 12, 31, 23, 59, 59, 999999), AAA, NORMAL),
    1346                  (datetime(2020, 1, 1), AAA, NORMAL),
    1347                  (datetime(2020, 2, 29), AAA, NORMAL),
    1348                  (datetime(2020, 3, 1, 11, 59), AAA, NORMAL),
    1349                  (datetime(2020, 3, 1, 12, fold=0), AAA, GAP),
    1350                  (datetime(2020, 3, 1, 12, fold=1), BBB, GAP),
    1351                  (datetime(2020, 3, 1, 13), BBB, NORMAL),
    1352                  (datetime(2020, 11, 1, 10, 59), BBB, NORMAL),
    1353                  (datetime(2020, 11, 1, 11, fold=0), BBB, FOLD),
    1354                  (datetime(2020, 11, 1, 11, fold=1), AAA, FOLD),
    1355                  (datetime(2020, 11, 1, 12), AAA, NORMAL),
    1356                  (datetime(2020, 12, 31, 23, 59, 59, 999999), AAA, NORMAL),
    1357              )
    1358  
    1359          @call
    1360          def _add():
    1361              # Taken from America/Godthab, this rule has a transition on the
    1362              # Saturday before the last Sunday of March and October, at 22:00
    1363              # and 23:00, respectively. This is encoded with negative start
    1364              # and end transition times.
    1365              tzstr = "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"
    1366  
    1367              N03 = ZoneOffset("-03", timedelta(hours=-3))
    1368              N02 = ZoneOffset("-02", timedelta(hours=-2), ONE_H)
    1369  
    1370              cases[tzstr] = (
    1371                  (datetime(2020, 3, 27), N03, NORMAL),
    1372                  (datetime(2020, 3, 28, 21, 59, 59), N03, NORMAL),
    1373                  (datetime(2020, 3, 28, 22, fold=0), N03, GAP),
    1374                  (datetime(2020, 3, 28, 22, fold=1), N02, GAP),
    1375                  (datetime(2020, 3, 28, 23), N02, NORMAL),
    1376                  (datetime(2020, 10, 24, 21), N02, NORMAL),
    1377                  (datetime(2020, 10, 24, 22, fold=0), N02, FOLD),
    1378                  (datetime(2020, 10, 24, 22, fold=1), N03, FOLD),
    1379                  (datetime(2020, 10, 24, 23), N03, NORMAL),
    1380              )
    1381  
    1382          @call
    1383          def _add():
    1384              # Transition times with minutes and seconds
    1385              tzstr = "AAA3BBB,M3.2.0/01:30,M11.1.0/02:15:45"
    1386  
    1387              AAA = ZoneOffset("AAA", timedelta(hours=-3))
    1388              BBB = ZoneOffset("BBB", timedelta(hours=-2), ONE_H)
    1389  
    1390              cases[tzstr] = (
    1391                  (datetime(2012, 3, 11, 1, 0), AAA, NORMAL),
    1392                  (datetime(2012, 3, 11, 1, 30, fold=0), AAA, GAP),
    1393                  (datetime(2012, 3, 11, 1, 30, fold=1), BBB, GAP),
    1394                  (datetime(2012, 3, 11, 2, 30), BBB, NORMAL),
    1395                  (datetime(2012, 11, 4, 1, 15, 44, 999999), BBB, NORMAL),
    1396                  (datetime(2012, 11, 4, 1, 15, 45, fold=0), BBB, FOLD),
    1397                  (datetime(2012, 11, 4, 1, 15, 45, fold=1), AAA, FOLD),
    1398                  (datetime(2012, 11, 4, 2, 15, 45), AAA, NORMAL),
    1399              )
    1400  
    1401          cls.test_cases = cases
    1402  
    1403  
    1404  class ESC[4;38;5;81mCTZStrTest(ESC[4;38;5;149mTZStrTest):
    1405      module = c_zoneinfo
    1406  
    1407  
    1408  class ESC[4;38;5;81mZoneInfoCacheTest(ESC[4;38;5;149mTzPathUserMixin, ESC[4;38;5;149mZoneInfoTestBase):
    1409      module = py_zoneinfo
    1410  
    1411      def setUp(self):
    1412          self.klass.clear_cache()
    1413          super().setUp()
    1414  
    1415      @property
    1416      def zoneinfo_data(self):
    1417          return ZONEINFO_DATA
    1418  
    1419      @property
    1420      def tzpath(self):
    1421          return [self.zoneinfo_data.tzpath]
    1422  
    1423      def test_ephemeral_zones(self):
    1424          self.assertIs(
    1425              self.klass("America/Los_Angeles"), self.klass("America/Los_Angeles")
    1426          )
    1427  
    1428      def test_strong_refs(self):
    1429          tz0 = self.klass("Australia/Sydney")
    1430          tz1 = self.klass("Australia/Sydney")
    1431  
    1432          self.assertIs(tz0, tz1)
    1433  
    1434      def test_no_cache(self):
    1435  
    1436          tz0 = self.klass("Europe/Lisbon")
    1437          tz1 = self.klass.no_cache("Europe/Lisbon")
    1438  
    1439          self.assertIsNot(tz0, tz1)
    1440  
    1441      def test_cache_reset_tzpath(self):
    1442          """Test that the cache persists when tzpath has been changed.
    1443  
    1444          The PEP specifies that as long as a reference exists to one zone
    1445          with a given key, the primary constructor must continue to return
    1446          the same object.
    1447          """
    1448          zi0 = self.klass("America/Los_Angeles")
    1449          with self.tzpath_context([]):
    1450              zi1 = self.klass("America/Los_Angeles")
    1451  
    1452          self.assertIs(zi0, zi1)
    1453  
    1454      def test_clear_cache_explicit_none(self):
    1455          la0 = self.klass("America/Los_Angeles")
    1456          self.klass.clear_cache(only_keys=None)
    1457          la1 = self.klass("America/Los_Angeles")
    1458  
    1459          self.assertIsNot(la0, la1)
    1460  
    1461      def test_clear_cache_one_key(self):
    1462          """Tests that you can clear a single key from the cache."""
    1463          la0 = self.klass("America/Los_Angeles")
    1464          dub0 = self.klass("Europe/Dublin")
    1465  
    1466          self.klass.clear_cache(only_keys=["America/Los_Angeles"])
    1467  
    1468          la1 = self.klass("America/Los_Angeles")
    1469          dub1 = self.klass("Europe/Dublin")
    1470  
    1471          self.assertIsNot(la0, la1)
    1472          self.assertIs(dub0, dub1)
    1473  
    1474      def test_clear_cache_two_keys(self):
    1475          la0 = self.klass("America/Los_Angeles")
    1476          dub0 = self.klass("Europe/Dublin")
    1477          tok0 = self.klass("Asia/Tokyo")
    1478  
    1479          self.klass.clear_cache(
    1480              only_keys=["America/Los_Angeles", "Europe/Dublin"]
    1481          )
    1482  
    1483          la1 = self.klass("America/Los_Angeles")
    1484          dub1 = self.klass("Europe/Dublin")
    1485          tok1 = self.klass("Asia/Tokyo")
    1486  
    1487          self.assertIsNot(la0, la1)
    1488          self.assertIsNot(dub0, dub1)
    1489          self.assertIs(tok0, tok1)
    1490  
    1491  
    1492  class ESC[4;38;5;81mCZoneInfoCacheTest(ESC[4;38;5;149mZoneInfoCacheTest):
    1493      module = c_zoneinfo
    1494  
    1495  
    1496  class ESC[4;38;5;81mZoneInfoPickleTest(ESC[4;38;5;149mTzPathUserMixin, ESC[4;38;5;149mZoneInfoTestBase):
    1497      module = py_zoneinfo
    1498  
    1499      def setUp(self):
    1500          self.klass.clear_cache()
    1501  
    1502          with contextlib.ExitStack() as stack:
    1503              stack.enter_context(test_support.set_zoneinfo_module(self.module))
    1504              self.addCleanup(stack.pop_all().close)
    1505  
    1506          super().setUp()
    1507  
    1508      @property
    1509      def zoneinfo_data(self):
    1510          return ZONEINFO_DATA
    1511  
    1512      @property
    1513      def tzpath(self):
    1514          return [self.zoneinfo_data.tzpath]
    1515  
    1516      def test_cache_hit(self):
    1517          for proto in range(pickle.HIGHEST_PROTOCOL + 1):
    1518              with self.subTest(proto=proto):
    1519                  zi_in = self.klass("Europe/Dublin")
    1520                  pkl = pickle.dumps(zi_in, protocol=proto)
    1521                  zi_rt = pickle.loads(pkl)
    1522  
    1523                  with self.subTest(test="Is non-pickled ZoneInfo"):
    1524                      self.assertIs(zi_in, zi_rt)
    1525  
    1526                  zi_rt2 = pickle.loads(pkl)
    1527                  with self.subTest(test="Is unpickled ZoneInfo"):
    1528                      self.assertIs(zi_rt, zi_rt2)
    1529  
    1530      def test_cache_miss(self):
    1531          for proto in range(pickle.HIGHEST_PROTOCOL + 1):
    1532              with self.subTest(proto=proto):
    1533                  zi_in = self.klass("Europe/Dublin")
    1534                  pkl = pickle.dumps(zi_in, protocol=proto)
    1535  
    1536                  del zi_in
    1537                  self.klass.clear_cache()  # Induce a cache miss
    1538                  zi_rt = pickle.loads(pkl)
    1539                  zi_rt2 = pickle.loads(pkl)
    1540  
    1541                  self.assertIs(zi_rt, zi_rt2)
    1542  
    1543      def test_no_cache(self):
    1544          for proto in range(pickle.HIGHEST_PROTOCOL + 1):
    1545              with self.subTest(proto=proto):
    1546                  zi_no_cache = self.klass.no_cache("Europe/Dublin")
    1547  
    1548                  pkl = pickle.dumps(zi_no_cache, protocol=proto)
    1549                  zi_rt = pickle.loads(pkl)
    1550  
    1551                  with self.subTest(test="Not the pickled object"):
    1552                      self.assertIsNot(zi_rt, zi_no_cache)
    1553  
    1554                  zi_rt2 = pickle.loads(pkl)
    1555                  with self.subTest(test="Not a second unpickled object"):
    1556                      self.assertIsNot(zi_rt, zi_rt2)
    1557  
    1558                  zi_cache = self.klass("Europe/Dublin")
    1559                  with self.subTest(test="Not a cached object"):
    1560                      self.assertIsNot(zi_rt, zi_cache)
    1561  
    1562      def test_from_file(self):
    1563          key = "Europe/Dublin"
    1564          with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
    1565              zi_nokey = self.klass.from_file(f)
    1566  
    1567              f.seek(0)
    1568              zi_key = self.klass.from_file(f, key=key)
    1569  
    1570          test_cases = [
    1571              (zi_key, "ZoneInfo with key"),
    1572              (zi_nokey, "ZoneInfo without key"),
    1573          ]
    1574  
    1575          for zi, test_name in test_cases:
    1576              for proto in range(pickle.HIGHEST_PROTOCOL + 1):
    1577                  with self.subTest(test_name=test_name, proto=proto):
    1578                      with self.assertRaises(pickle.PicklingError):
    1579                          pickle.dumps(zi, protocol=proto)
    1580  
    1581      def test_pickle_after_from_file(self):
    1582          # This may be a bit of paranoia, but this test is to ensure that no
    1583          # global state is maintained in order to handle the pickle cache and
    1584          # from_file behavior, and that it is possible to interweave the
    1585          # constructors of each of these and pickling/unpickling without issues.
    1586          for proto in range(pickle.HIGHEST_PROTOCOL + 1):
    1587              with self.subTest(proto=proto):
    1588                  key = "Europe/Dublin"
    1589                  zi = self.klass(key)
    1590  
    1591                  pkl_0 = pickle.dumps(zi, protocol=proto)
    1592                  zi_rt_0 = pickle.loads(pkl_0)
    1593                  self.assertIs(zi, zi_rt_0)
    1594  
    1595                  with open(self.zoneinfo_data.path_from_key(key), "rb") as f:
    1596                      zi_ff = self.klass.from_file(f, key=key)
    1597  
    1598                  pkl_1 = pickle.dumps(zi, protocol=proto)
    1599                  zi_rt_1 = pickle.loads(pkl_1)
    1600                  self.assertIs(zi, zi_rt_1)
    1601  
    1602                  with self.assertRaises(pickle.PicklingError):
    1603                      pickle.dumps(zi_ff, protocol=proto)
    1604  
    1605                  pkl_2 = pickle.dumps(zi, protocol=proto)
    1606                  zi_rt_2 = pickle.loads(pkl_2)
    1607                  self.assertIs(zi, zi_rt_2)
    1608  
    1609  
    1610  class ESC[4;38;5;81mCZoneInfoPickleTest(ESC[4;38;5;149mZoneInfoPickleTest):
    1611      module = c_zoneinfo
    1612  
    1613  
    1614  class ESC[4;38;5;81mCallingConventionTest(ESC[4;38;5;149mZoneInfoTestBase):
    1615      """Tests for functions with restricted calling conventions."""
    1616  
    1617      module = py_zoneinfo
    1618  
    1619      @property
    1620      def zoneinfo_data(self):
    1621          return ZONEINFO_DATA
    1622  
    1623      def test_from_file(self):
    1624          with open(self.zoneinfo_data.path_from_key("UTC"), "rb") as f:
    1625              with self.assertRaises(TypeError):
    1626                  self.klass.from_file(fobj=f)
    1627  
    1628      def test_clear_cache(self):
    1629          with self.assertRaises(TypeError):
    1630              self.klass.clear_cache(["UTC"])
    1631  
    1632  
    1633  class ESC[4;38;5;81mCCallingConventionTest(ESC[4;38;5;149mCallingConventionTest):
    1634      module = c_zoneinfo
    1635  
    1636  
    1637  class ESC[4;38;5;81mTzPathTest(ESC[4;38;5;149mTzPathUserMixin, ESC[4;38;5;149mZoneInfoTestBase):
    1638      module = py_zoneinfo
    1639  
    1640      @staticmethod
    1641      @contextlib.contextmanager
    1642      def python_tzpath_context(value):
    1643          path_var = "PYTHONTZPATH"
    1644          unset_env_sentinel = object()
    1645          old_env = unset_env_sentinel
    1646          try:
    1647              with OS_ENV_LOCK:
    1648                  old_env = os.environ.get(path_var, None)
    1649                  os.environ[path_var] = value
    1650                  yield
    1651          finally:
    1652              if old_env is unset_env_sentinel:
    1653                  # In this case, `old_env` was never retrieved from the
    1654                  # environment for whatever reason, so there's no need to
    1655                  # reset the environment TZPATH.
    1656                  pass
    1657              elif old_env is None:
    1658                  del os.environ[path_var]
    1659              else:
    1660                  os.environ[path_var] = old_env  # pragma: nocover
    1661  
    1662      def test_env_variable(self):
    1663          """Tests that the environment variable works with reset_tzpath."""
    1664          new_paths = [
    1665              ("", []),
    1666              ("/etc/zoneinfo", ["/etc/zoneinfo"]),
    1667              (f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]),
    1668          ]
    1669  
    1670          for new_path_var, expected_result in new_paths:
    1671              with self.python_tzpath_context(new_path_var):
    1672                  with self.subTest(tzpath=new_path_var):
    1673                      self.module.reset_tzpath()
    1674                      tzpath = self.module.TZPATH
    1675                      self.assertSequenceEqual(tzpath, expected_result)
    1676  
    1677      def test_env_variable_relative_paths(self):
    1678          test_cases = [
    1679              [("path/to/somewhere",), ()],
    1680              [
    1681                  ("/usr/share/zoneinfo", "path/to/somewhere",),
    1682                  ("/usr/share/zoneinfo",),
    1683              ],
    1684              [("../relative/path",), ()],
    1685              [
    1686                  ("/usr/share/zoneinfo", "../relative/path",),
    1687                  ("/usr/share/zoneinfo",),
    1688              ],
    1689              [("path/to/somewhere", "../relative/path",), ()],
    1690              [
    1691                  (
    1692                      "/usr/share/zoneinfo",
    1693                      "path/to/somewhere",
    1694                      "../relative/path",
    1695                  ),
    1696                  ("/usr/share/zoneinfo",),
    1697              ],
    1698          ]
    1699  
    1700          for input_paths, expected_paths in test_cases:
    1701              path_var = os.pathsep.join(input_paths)
    1702              with self.python_tzpath_context(path_var):
    1703                  with self.subTest("warning", path_var=path_var):
    1704                      # Note: Per PEP 615 the warning is implementation-defined
    1705                      # behavior, other implementations need not warn.
    1706                      with self.assertWarns(self.module.InvalidTZPathWarning):
    1707                          self.module.reset_tzpath()
    1708  
    1709                  tzpath = self.module.TZPATH
    1710                  with self.subTest("filtered", path_var=path_var):
    1711                      self.assertSequenceEqual(tzpath, expected_paths)
    1712  
    1713      def test_reset_tzpath_kwarg(self):
    1714          self.module.reset_tzpath(to=["/a/b/c"])
    1715  
    1716          self.assertSequenceEqual(self.module.TZPATH, ("/a/b/c",))
    1717  
    1718      def test_reset_tzpath_relative_paths(self):
    1719          bad_values = [
    1720              ("path/to/somewhere",),
    1721              ("/usr/share/zoneinfo", "path/to/somewhere",),
    1722              ("../relative/path",),
    1723              ("/usr/share/zoneinfo", "../relative/path",),
    1724              ("path/to/somewhere", "../relative/path",),
    1725              ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",),
    1726          ]
    1727          for input_paths in bad_values:
    1728              with self.subTest(input_paths=input_paths):
    1729                  with self.assertRaises(ValueError):
    1730                      self.module.reset_tzpath(to=input_paths)
    1731  
    1732      def test_tzpath_type_error(self):
    1733          bad_values = [
    1734              "/etc/zoneinfo:/usr/share/zoneinfo",
    1735              b"/etc/zoneinfo:/usr/share/zoneinfo",
    1736              0,
    1737          ]
    1738  
    1739          for bad_value in bad_values:
    1740              with self.subTest(value=bad_value):
    1741                  with self.assertRaises(TypeError):
    1742                      self.module.reset_tzpath(bad_value)
    1743  
    1744      def test_tzpath_attribute(self):
    1745          tzpath_0 = ["/one", "/two"]
    1746          tzpath_1 = ["/three"]
    1747  
    1748          with self.tzpath_context(tzpath_0):
    1749              query_0 = self.module.TZPATH
    1750  
    1751          with self.tzpath_context(tzpath_1):
    1752              query_1 = self.module.TZPATH
    1753  
    1754          self.assertSequenceEqual(tzpath_0, query_0)
    1755          self.assertSequenceEqual(tzpath_1, query_1)
    1756  
    1757  
    1758  class ESC[4;38;5;81mCTzPathTest(ESC[4;38;5;149mTzPathTest):
    1759      module = c_zoneinfo
    1760  
    1761  
    1762  class ESC[4;38;5;81mTestModule(ESC[4;38;5;149mZoneInfoTestBase):
    1763      module = py_zoneinfo
    1764  
    1765      @property
    1766      def zoneinfo_data(self):
    1767          return ZONEINFO_DATA
    1768  
    1769      @cached_property
    1770      def _UTC_bytes(self):
    1771          zone_file = self.zoneinfo_data.path_from_key("UTC")
    1772          with open(zone_file, "rb") as f:
    1773              return f.read()
    1774  
    1775      def touch_zone(self, key, tz_root):
    1776          """Creates a valid TZif file at key under the zoneinfo root tz_root.
    1777  
    1778          tz_root must exist, but all folders below that will be created.
    1779          """
    1780          if not os.path.exists(tz_root):
    1781              raise FileNotFoundError(f"{tz_root} does not exist.")
    1782  
    1783          root_dir, *tail = key.rsplit("/", 1)
    1784          if tail:  # If there's no tail, then the first component isn't a dir
    1785              os.makedirs(os.path.join(tz_root, root_dir), exist_ok=True)
    1786  
    1787          zonefile_path = os.path.join(tz_root, key)
    1788          with open(zonefile_path, "wb") as f:
    1789              f.write(self._UTC_bytes)
    1790  
    1791      def test_getattr_error(self):
    1792          with self.assertRaises(AttributeError):
    1793              self.module.NOATTRIBUTE
    1794  
    1795      def test_dir_contains_all(self):
    1796          """dir(self.module) should at least contain everything in __all__."""
    1797          module_all_set = set(self.module.__all__)
    1798          module_dir_set = set(dir(self.module))
    1799  
    1800          difference = module_all_set - module_dir_set
    1801  
    1802          self.assertFalse(difference)
    1803  
    1804      def test_dir_unique(self):
    1805          """Test that there are no duplicates in dir(self.module)"""
    1806          module_dir = dir(self.module)
    1807          module_unique = set(module_dir)
    1808  
    1809          self.assertCountEqual(module_dir, module_unique)
    1810  
    1811      def test_available_timezones(self):
    1812          with self.tzpath_context([self.zoneinfo_data.tzpath]):
    1813              self.assertTrue(self.zoneinfo_data.keys)  # Sanity check
    1814  
    1815              available_keys = self.module.available_timezones()
    1816              zoneinfo_keys = set(self.zoneinfo_data.keys)
    1817  
    1818              # If tzdata is not present, zoneinfo_keys == available_keys,
    1819              # otherwise it should be a subset.
    1820              union = zoneinfo_keys & available_keys
    1821              self.assertEqual(zoneinfo_keys, union)
    1822  
    1823      def test_available_timezones_weirdzone(self):
    1824          with tempfile.TemporaryDirectory() as td:
    1825              # Make a fictional zone at "Mars/Olympus_Mons"
    1826              self.touch_zone("Mars/Olympus_Mons", td)
    1827  
    1828              with self.tzpath_context([td]):
    1829                  available_keys = self.module.available_timezones()
    1830                  self.assertIn("Mars/Olympus_Mons", available_keys)
    1831  
    1832      def test_folder_exclusions(self):
    1833          expected = {
    1834              "America/Los_Angeles",
    1835              "America/Santiago",
    1836              "America/Indiana/Indianapolis",
    1837              "UTC",
    1838              "Europe/Paris",
    1839              "Europe/London",
    1840              "Asia/Tokyo",
    1841              "Australia/Sydney",
    1842          }
    1843  
    1844          base_tree = list(expected)
    1845          posix_tree = [f"posix/{x}" for x in base_tree]
    1846          right_tree = [f"right/{x}" for x in base_tree]
    1847  
    1848          cases = [
    1849              ("base_tree", base_tree),
    1850              ("base_and_posix", base_tree + posix_tree),
    1851              ("base_and_right", base_tree + right_tree),
    1852              ("all_trees", base_tree + right_tree + posix_tree),
    1853          ]
    1854  
    1855          with tempfile.TemporaryDirectory() as td:
    1856              for case_name, tree in cases:
    1857                  tz_root = os.path.join(td, case_name)
    1858                  os.mkdir(tz_root)
    1859  
    1860                  for key in tree:
    1861                      self.touch_zone(key, tz_root)
    1862  
    1863                  with self.tzpath_context([tz_root]):
    1864                      with self.subTest(case_name):
    1865                          actual = self.module.available_timezones()
    1866                          self.assertEqual(actual, expected)
    1867  
    1868      def test_exclude_posixrules(self):
    1869          expected = {
    1870              "America/New_York",
    1871              "Europe/London",
    1872          }
    1873  
    1874          tree = list(expected) + ["posixrules"]
    1875  
    1876          with tempfile.TemporaryDirectory() as td:
    1877              for key in tree:
    1878                  self.touch_zone(key, td)
    1879  
    1880              with self.tzpath_context([td]):
    1881                  actual = self.module.available_timezones()
    1882                  self.assertEqual(actual, expected)
    1883  
    1884  
    1885  class ESC[4;38;5;81mCTestModule(ESC[4;38;5;149mTestModule):
    1886      module = c_zoneinfo
    1887  
    1888  
    1889  class ESC[4;38;5;81mExtensionBuiltTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
    1890      """Smoke test to ensure that the C and Python extensions are both tested.
    1891  
    1892      Because the intention is for the Python and C versions of ZoneInfo to
    1893      behave identically, these tests necessarily rely on implementation details,
    1894      so the tests may need to be adjusted if the implementations change. Do not
    1895      rely on these tests as an indication of stable properties of these classes.
    1896      """
    1897  
    1898      def test_cache_location(self):
    1899          # The pure Python version stores caches on attributes, but the C
    1900          # extension stores them in C globals (at least for now)
    1901          self.assertFalse(hasattr(c_zoneinfo.ZoneInfo, "_weak_cache"))
    1902          self.assertTrue(hasattr(py_zoneinfo.ZoneInfo, "_weak_cache"))
    1903  
    1904      def test_gc_tracked(self):
    1905          # The pure Python version is tracked by the GC but (for now) the C
    1906          # version is not.
    1907          import gc
    1908  
    1909          self.assertTrue(gc.is_tracked(py_zoneinfo.ZoneInfo))
    1910          self.assertFalse(gc.is_tracked(c_zoneinfo.ZoneInfo))
    1911  
    1912  
    1913  @dataclasses.dataclass(frozen=True)
    1914  class ESC[4;38;5;81mZoneOffset:
    1915      tzname: str
    1916      utcoffset: timedelta
    1917      dst: timedelta = ZERO
    1918  
    1919  
    1920  @dataclasses.dataclass(frozen=True)
    1921  class ESC[4;38;5;81mZoneTransition:
    1922      transition: datetime
    1923      offset_before: ZoneOffset
    1924      offset_after: ZoneOffset
    1925  
    1926      @property
    1927      def transition_utc(self):
    1928          return (self.transition - self.offset_before.utcoffset).replace(
    1929              tzinfo=timezone.utc
    1930          )
    1931  
    1932      @property
    1933      def fold(self):
    1934          """Whether this introduces a fold"""
    1935          return self.offset_before.utcoffset > self.offset_after.utcoffset
    1936  
    1937      @property
    1938      def gap(self):
    1939          """Whether this introduces a gap"""
    1940          return self.offset_before.utcoffset < self.offset_after.utcoffset
    1941  
    1942      @property
    1943      def delta(self):
    1944          return self.offset_after.utcoffset - self.offset_before.utcoffset
    1945  
    1946      @property
    1947      def anomaly_start(self):
    1948          if self.fold:
    1949              return self.transition + self.delta
    1950          else:
    1951              return self.transition
    1952  
    1953      @property
    1954      def anomaly_end(self):
    1955          if not self.fold:
    1956              return self.transition + self.delta
    1957          else:
    1958              return self.transition
    1959  
    1960  
    1961  class ESC[4;38;5;81mZoneInfoData:
    1962      def __init__(self, source_json, tzpath, v1=False):
    1963          self.tzpath = pathlib.Path(tzpath)
    1964          self.keys = []
    1965          self.v1 = v1
    1966          self._populate_tzpath(source_json)
    1967  
    1968      def path_from_key(self, key):
    1969          return self.tzpath / key
    1970  
    1971      def _populate_tzpath(self, source_json):
    1972          with open(source_json, "rb") as f:
    1973              zoneinfo_dict = json.load(f)
    1974  
    1975          zoneinfo_data = zoneinfo_dict["data"]
    1976  
    1977          for key, value in zoneinfo_data.items():
    1978              self.keys.append(key)
    1979              raw_data = self._decode_text(value)
    1980  
    1981              if self.v1:
    1982                  data = self._convert_to_v1(raw_data)
    1983              else:
    1984                  data = raw_data
    1985  
    1986              destination = self.path_from_key(key)
    1987              destination.parent.mkdir(exist_ok=True, parents=True)
    1988              with open(destination, "wb") as f:
    1989                  f.write(data)
    1990  
    1991      def _decode_text(self, contents):
    1992          raw_data = b"".join(map(str.encode, contents))
    1993          decoded = base64.b85decode(raw_data)
    1994  
    1995          return lzma.decompress(decoded)
    1996  
    1997      def _convert_to_v1(self, contents):
    1998          assert contents[0:4] == b"TZif", "Invalid TZif data found!"
    1999          version = int(contents[4:5])
    2000  
    2001          header_start = 4 + 16
    2002          header_end = header_start + 24  # 6l == 24 bytes
    2003          assert version >= 2, "Version 1 file found: no conversion necessary"
    2004          isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt = struct.unpack(
    2005              ">6l", contents[header_start:header_end]
    2006          )
    2007  
    2008          file_size = (
    2009              timecnt * 5
    2010              + typecnt * 6
    2011              + charcnt
    2012              + leapcnt * 8
    2013              + isstdcnt
    2014              + isutcnt
    2015          )
    2016          file_size += header_end
    2017          out = b"TZif" + b"\x00" + contents[5:file_size]
    2018  
    2019          assert (
    2020              contents[file_size : (file_size + 4)] == b"TZif"
    2021          ), "Version 2 file not truncated at Version 2 header"
    2022  
    2023          return out
    2024  
    2025  
    2026  class ESC[4;38;5;81mZoneDumpData:
    2027      @classmethod
    2028      def transition_keys(cls):
    2029          return cls._get_zonedump().keys()
    2030  
    2031      @classmethod
    2032      def load_transition_examples(cls, key):
    2033          return cls._get_zonedump()[key]
    2034  
    2035      @classmethod
    2036      def fixed_offset_zones(cls):
    2037          if not cls._FIXED_OFFSET_ZONES:
    2038              cls._populate_fixed_offsets()
    2039  
    2040          return cls._FIXED_OFFSET_ZONES.items()
    2041  
    2042      @classmethod
    2043      def _get_zonedump(cls):
    2044          if not cls._ZONEDUMP_DATA:
    2045              cls._populate_zonedump_data()
    2046          return cls._ZONEDUMP_DATA
    2047  
    2048      @classmethod
    2049      def _populate_fixed_offsets(cls):
    2050          cls._FIXED_OFFSET_ZONES = {
    2051              "UTC": ZoneOffset("UTC", ZERO, ZERO),
    2052          }
    2053  
    2054      @classmethod
    2055      def _populate_zonedump_data(cls):
    2056          def _Africa_Abidjan():
    2057              LMT = ZoneOffset("LMT", timedelta(seconds=-968))
    2058              GMT = ZoneOffset("GMT", ZERO)
    2059  
    2060              return [
    2061                  ZoneTransition(datetime(1912, 1, 1), LMT, GMT),
    2062              ]
    2063  
    2064          def _Africa_Casablanca():
    2065              P00_s = ZoneOffset("+00", ZERO, ZERO)
    2066              P01_d = ZoneOffset("+01", ONE_H, ONE_H)
    2067              P00_d = ZoneOffset("+00", ZERO, -ONE_H)
    2068              P01_s = ZoneOffset("+01", ONE_H, ZERO)
    2069  
    2070              return [
    2071                  # Morocco sometimes pauses DST during Ramadan
    2072                  ZoneTransition(datetime(2018, 3, 25, 2), P00_s, P01_d),
    2073                  ZoneTransition(datetime(2018, 5, 13, 3), P01_d, P00_s),
    2074                  ZoneTransition(datetime(2018, 6, 17, 2), P00_s, P01_d),
    2075                  # On October 28th Morocco set standard time to +01,
    2076                  # with negative DST only during Ramadan
    2077                  ZoneTransition(datetime(2018, 10, 28, 3), P01_d, P01_s),
    2078                  ZoneTransition(datetime(2019, 5, 5, 3), P01_s, P00_d),
    2079                  ZoneTransition(datetime(2019, 6, 9, 2), P00_d, P01_s),
    2080              ]
    2081  
    2082          def _America_Los_Angeles():
    2083              LMT = ZoneOffset("LMT", timedelta(seconds=-28378), ZERO)
    2084              PST = ZoneOffset("PST", timedelta(hours=-8), ZERO)
    2085              PDT = ZoneOffset("PDT", timedelta(hours=-7), ONE_H)
    2086              PWT = ZoneOffset("PWT", timedelta(hours=-7), ONE_H)
    2087              PPT = ZoneOffset("PPT", timedelta(hours=-7), ONE_H)
    2088  
    2089              return [
    2090                  ZoneTransition(datetime(1883, 11, 18, 12, 7, 2), LMT, PST),
    2091                  ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
    2092                  ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT),
    2093                  ZoneTransition(datetime(1918, 10, 27, 2), PDT, PST),
    2094                  # Transition to Pacific War Time
    2095                  ZoneTransition(datetime(1942, 2, 9, 2), PST, PWT),
    2096                  # Transition from Pacific War Time to Pacific Peace Time
    2097                  ZoneTransition(datetime(1945, 8, 14, 16), PWT, PPT),
    2098                  ZoneTransition(datetime(1945, 9, 30, 2), PPT, PST),
    2099                  ZoneTransition(datetime(2015, 3, 8, 2), PST, PDT),
    2100                  ZoneTransition(datetime(2015, 11, 1, 2), PDT, PST),
    2101                  # After 2038: Rules continue indefinitely
    2102                  ZoneTransition(datetime(2450, 3, 13, 2), PST, PDT),
    2103                  ZoneTransition(datetime(2450, 11, 6, 2), PDT, PST),
    2104              ]
    2105  
    2106          def _America_Santiago():
    2107              LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO)
    2108              SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO)
    2109              N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO)
    2110              N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO)
    2111              N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H)
    2112  
    2113              return [
    2114                  ZoneTransition(datetime(1890, 1, 1), LMT, SMT),
    2115                  ZoneTransition(datetime(1910, 1, 10), SMT, N05),
    2116                  ZoneTransition(datetime(1916, 7, 1), N05, SMT),
    2117                  ZoneTransition(datetime(2008, 3, 30), N03, N04),
    2118                  ZoneTransition(datetime(2008, 10, 12), N04, N03),
    2119                  ZoneTransition(datetime(2040, 4, 8), N03, N04),
    2120                  ZoneTransition(datetime(2040, 9, 2), N04, N03),
    2121              ]
    2122  
    2123          def _Asia_Tokyo():
    2124              JST = ZoneOffset("JST", timedelta(seconds=32400), ZERO)
    2125              JDT = ZoneOffset("JDT", timedelta(seconds=36000), ONE_H)
    2126  
    2127              # Japan had DST from 1948 to 1951, and it was unusual in that
    2128              # the transition from DST to STD occurred at 25:00, and is
    2129              # denominated as such in the time zone database
    2130              return [
    2131                  ZoneTransition(datetime(1948, 5, 2), JST, JDT),
    2132                  ZoneTransition(datetime(1948, 9, 12, 1), JDT, JST),
    2133                  ZoneTransition(datetime(1951, 9, 9, 1), JDT, JST),
    2134              ]
    2135  
    2136          def _Australia_Sydney():
    2137              LMT = ZoneOffset("LMT", timedelta(seconds=36292), ZERO)
    2138              AEST = ZoneOffset("AEST", timedelta(seconds=36000), ZERO)
    2139              AEDT = ZoneOffset("AEDT", timedelta(seconds=39600), ONE_H)
    2140  
    2141              return [
    2142                  ZoneTransition(datetime(1895, 2, 1), LMT, AEST),
    2143                  ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT),
    2144                  ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST),
    2145                  ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST),
    2146                  ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT),
    2147                  ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST),
    2148                  ZoneTransition(datetime(2040, 10, 7, 2), AEST, AEDT),
    2149              ]
    2150  
    2151          def _Europe_Dublin():
    2152              LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO)
    2153              DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO)
    2154              IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H)
    2155              GMT_0 = ZoneOffset("GMT", ZERO, ZERO)
    2156              BST = ZoneOffset("BST", ONE_H, ONE_H)
    2157              GMT_1 = ZoneOffset("GMT", ZERO, -ONE_H)
    2158              IST_1 = ZoneOffset("IST", ONE_H, ZERO)
    2159  
    2160              return [
    2161                  ZoneTransition(datetime(1880, 8, 2, 0), LMT, DMT),
    2162                  ZoneTransition(datetime(1916, 5, 21, 2), DMT, IST_0),
    2163                  ZoneTransition(datetime(1916, 10, 1, 3), IST_0, GMT_0),
    2164                  ZoneTransition(datetime(1917, 4, 8, 2), GMT_0, BST),
    2165                  ZoneTransition(datetime(2016, 3, 27, 1), GMT_1, IST_1),
    2166                  ZoneTransition(datetime(2016, 10, 30, 2), IST_1, GMT_1),
    2167                  ZoneTransition(datetime(2487, 3, 30, 1), GMT_1, IST_1),
    2168                  ZoneTransition(datetime(2487, 10, 26, 2), IST_1, GMT_1),
    2169              ]
    2170  
    2171          def _Europe_Lisbon():
    2172              WET = ZoneOffset("WET", ZERO, ZERO)
    2173              WEST = ZoneOffset("WEST", ONE_H, ONE_H)
    2174              CET = ZoneOffset("CET", ONE_H, ZERO)
    2175              CEST = ZoneOffset("CEST", timedelta(seconds=7200), ONE_H)
    2176  
    2177              return [
    2178                  ZoneTransition(datetime(1992, 3, 29, 1), WET, WEST),
    2179                  ZoneTransition(datetime(1992, 9, 27, 2), WEST, CET),
    2180                  ZoneTransition(datetime(1993, 3, 28, 2), CET, CEST),
    2181                  ZoneTransition(datetime(1993, 9, 26, 3), CEST, CET),
    2182                  ZoneTransition(datetime(1996, 3, 31, 2), CET, WEST),
    2183                  ZoneTransition(datetime(1996, 10, 27, 2), WEST, WET),
    2184              ]
    2185  
    2186          def _Europe_London():
    2187              LMT = ZoneOffset("LMT", timedelta(seconds=-75), ZERO)
    2188              GMT = ZoneOffset("GMT", ZERO, ZERO)
    2189              BST = ZoneOffset("BST", ONE_H, ONE_H)
    2190  
    2191              return [
    2192                  ZoneTransition(datetime(1847, 12, 1), LMT, GMT),
    2193                  ZoneTransition(datetime(2005, 3, 27, 1), GMT, BST),
    2194                  ZoneTransition(datetime(2005, 10, 30, 2), BST, GMT),
    2195                  ZoneTransition(datetime(2043, 3, 29, 1), GMT, BST),
    2196                  ZoneTransition(datetime(2043, 10, 25, 2), BST, GMT),
    2197              ]
    2198  
    2199          def _Pacific_Kiritimati():
    2200              LMT = ZoneOffset("LMT", timedelta(seconds=-37760), ZERO)
    2201              N1040 = ZoneOffset("-1040", timedelta(seconds=-38400), ZERO)
    2202              N10 = ZoneOffset("-10", timedelta(seconds=-36000), ZERO)
    2203              P14 = ZoneOffset("+14", timedelta(seconds=50400), ZERO)
    2204  
    2205              # This is literally every transition in Christmas Island history
    2206              return [
    2207                  ZoneTransition(datetime(1901, 1, 1), LMT, N1040),
    2208                  ZoneTransition(datetime(1979, 10, 1), N1040, N10),
    2209                  # They skipped December 31, 1994
    2210                  ZoneTransition(datetime(1994, 12, 31), N10, P14),
    2211              ]
    2212  
    2213          cls._ZONEDUMP_DATA = {
    2214              "Africa/Abidjan": _Africa_Abidjan(),
    2215              "Africa/Casablanca": _Africa_Casablanca(),
    2216              "America/Los_Angeles": _America_Los_Angeles(),
    2217              "America/Santiago": _America_Santiago(),
    2218              "Australia/Sydney": _Australia_Sydney(),
    2219              "Asia/Tokyo": _Asia_Tokyo(),
    2220              "Europe/Dublin": _Europe_Dublin(),
    2221              "Europe/Lisbon": _Europe_Lisbon(),
    2222              "Europe/London": _Europe_London(),
    2223              "Pacific/Kiritimati": _Pacific_Kiritimati(),
    2224          }
    2225  
    2226      _ZONEDUMP_DATA = None
    2227      _FIXED_OFFSET_ZONES = None
    2228  
    2229  
    2230  if __name__ == '__main__':
    2231      unittest.main()