1  import contextlib
       2  import datetime
       3  import os
       4  import pickle
       5  import unittest
       6  import zoneinfo
       7  
       8  from test.support.hypothesis_helper import hypothesis
       9  
      10  import test.test_zoneinfo._support as test_support
      11  
      12  ZoneInfoTestBase = test_support.ZoneInfoTestBase
      13  
      14  py_zoneinfo, c_zoneinfo = test_support.get_modules()
      15  
      16  UTC = datetime.timezone.utc
      17  MIN_UTC = datetime.datetime.min.replace(tzinfo=UTC)
      18  MAX_UTC = datetime.datetime.max.replace(tzinfo=UTC)
      19  ZERO = datetime.timedelta(0)
      20  
      21  
      22  def _valid_keys():
      23      """Get available time zones, including posix/ and right/ directories."""
      24      from importlib import resources
      25  
      26      available_zones = sorted(zoneinfo.available_timezones())
      27      TZPATH = zoneinfo.TZPATH
      28  
      29      def valid_key(key):
      30          for root in TZPATH:
      31              key_file = os.path.join(root, key)
      32              if os.path.exists(key_file):
      33                  return True
      34  
      35          components = key.split("/")
      36          package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
      37          resource_name = components[-1]
      38  
      39          try:
      40              return resources.files(package_name).joinpath(resource_name).is_file()
      41          except ModuleNotFoundError:
      42              return False
      43  
      44      # This relies on the fact that dictionaries maintain insertion order — for
      45      # shrinking purposes, it is preferable to start with the standard version,
      46      # then move to the posix/ version, then to the right/ version.
      47      out_zones = {"": available_zones}
      48      for prefix in ["posix", "right"]:
      49          prefix_out = []
      50          for key in available_zones:
      51              prefix_key = f"{prefix}/{key}"
      52              if valid_key(prefix_key):
      53                  prefix_out.append(prefix_key)
      54  
      55          out_zones[prefix] = prefix_out
      56  
      57      output = []
      58      for keys in out_zones.values():
      59          output.extend(keys)
      60  
      61      return output
      62  
      63  
      64  VALID_KEYS = _valid_keys()
      65  if not VALID_KEYS:
      66      raise unittest.SkipTest("No time zone data available")
      67  
      68  
      69  def valid_keys():
      70      return hypothesis.strategies.sampled_from(VALID_KEYS)
      71  
      72  
      73  KEY_EXAMPLES = [
      74      "Africa/Abidjan",
      75      "Africa/Casablanca",
      76      "America/Los_Angeles",
      77      "America/Santiago",
      78      "Asia/Tokyo",
      79      "Australia/Sydney",
      80      "Europe/Dublin",
      81      "Europe/Lisbon",
      82      "Europe/London",
      83      "Pacific/Kiritimati",
      84      "UTC",
      85  ]
      86  
      87  
      88  def add_key_examples(f):
      89      for key in KEY_EXAMPLES:
      90          f = hypothesis.example(key)(f)
      91      return f
      92  
      93  
      94  class ESC[4;38;5;81mZoneInfoTest(ESC[4;38;5;149mZoneInfoTestBase):
      95      module = py_zoneinfo
      96  
      97      @hypothesis.given(key=valid_keys())
      98      @add_key_examples
      99      def test_str(self, key):
     100          zi = self.klass(key)
     101          self.assertEqual(str(zi), key)
     102  
     103      @hypothesis.given(key=valid_keys())
     104      @add_key_examples
     105      def test_key(self, key):
     106          zi = self.klass(key)
     107  
     108          self.assertEqual(zi.key, key)
     109  
     110      @hypothesis.given(
     111          dt=hypothesis.strategies.one_of(
     112              hypothesis.strategies.datetimes(), hypothesis.strategies.times()
     113          )
     114      )
     115      @hypothesis.example(dt=datetime.datetime.min)
     116      @hypothesis.example(dt=datetime.datetime.max)
     117      @hypothesis.example(dt=datetime.datetime(1970, 1, 1))
     118      @hypothesis.example(dt=datetime.datetime(2039, 1, 1))
     119      @hypothesis.example(dt=datetime.time(0))
     120      @hypothesis.example(dt=datetime.time(12, 0))
     121      @hypothesis.example(dt=datetime.time(23, 59, 59, 999999))
     122      def test_utc(self, dt):
     123          zi = self.klass("UTC")
     124          dt_zi = dt.replace(tzinfo=zi)
     125  
     126          self.assertEqual(dt_zi.utcoffset(), ZERO)
     127          self.assertEqual(dt_zi.dst(), ZERO)
     128          self.assertEqual(dt_zi.tzname(), "UTC")
     129  
     130  
     131  class ESC[4;38;5;81mCZoneInfoTest(ESC[4;38;5;149mZoneInfoTest):
     132      module = c_zoneinfo
     133  
     134  
     135  class ESC[4;38;5;81mZoneInfoPickleTest(ESC[4;38;5;149mZoneInfoTestBase):
     136      module = py_zoneinfo
     137  
     138      def setUp(self):
     139          with contextlib.ExitStack() as stack:
     140              stack.enter_context(test_support.set_zoneinfo_module(self.module))
     141              self.addCleanup(stack.pop_all().close)
     142  
     143          super().setUp()
     144  
     145      @hypothesis.given(key=valid_keys())
     146      @add_key_examples
     147      def test_pickle_unpickle_cache(self, key):
     148          zi = self.klass(key)
     149          pkl_str = pickle.dumps(zi)
     150          zi_rt = pickle.loads(pkl_str)
     151  
     152          self.assertIs(zi, zi_rt)
     153  
     154      @hypothesis.given(key=valid_keys())
     155      @add_key_examples
     156      def test_pickle_unpickle_no_cache(self, key):
     157          zi = self.klass.no_cache(key)
     158          pkl_str = pickle.dumps(zi)
     159          zi_rt = pickle.loads(pkl_str)
     160  
     161          self.assertIsNot(zi, zi_rt)
     162          self.assertEqual(str(zi), str(zi_rt))
     163  
     164      @hypothesis.given(key=valid_keys())
     165      @add_key_examples
     166      def test_pickle_unpickle_cache_multiple_rounds(self, key):
     167          """Test that pickle/unpickle is idempotent."""
     168          zi_0 = self.klass(key)
     169          pkl_str_0 = pickle.dumps(zi_0)
     170          zi_1 = pickle.loads(pkl_str_0)
     171          pkl_str_1 = pickle.dumps(zi_1)
     172          zi_2 = pickle.loads(pkl_str_1)
     173          pkl_str_2 = pickle.dumps(zi_2)
     174  
     175          self.assertEqual(pkl_str_0, pkl_str_1)
     176          self.assertEqual(pkl_str_1, pkl_str_2)
     177  
     178          self.assertIs(zi_0, zi_1)
     179          self.assertIs(zi_0, zi_2)
     180          self.assertIs(zi_1, zi_2)
     181  
     182      @hypothesis.given(key=valid_keys())
     183      @add_key_examples
     184      def test_pickle_unpickle_no_cache_multiple_rounds(self, key):
     185          """Test that pickle/unpickle is idempotent."""
     186          zi_cache = self.klass(key)
     187  
     188          zi_0 = self.klass.no_cache(key)
     189          pkl_str_0 = pickle.dumps(zi_0)
     190          zi_1 = pickle.loads(pkl_str_0)
     191          pkl_str_1 = pickle.dumps(zi_1)
     192          zi_2 = pickle.loads(pkl_str_1)
     193          pkl_str_2 = pickle.dumps(zi_2)
     194  
     195          self.assertEqual(pkl_str_0, pkl_str_1)
     196          self.assertEqual(pkl_str_1, pkl_str_2)
     197  
     198          self.assertIsNot(zi_0, zi_1)
     199          self.assertIsNot(zi_0, zi_2)
     200          self.assertIsNot(zi_1, zi_2)
     201  
     202          self.assertIsNot(zi_0, zi_cache)
     203          self.assertIsNot(zi_1, zi_cache)
     204          self.assertIsNot(zi_2, zi_cache)
     205  
     206  
     207  class ESC[4;38;5;81mCZoneInfoPickleTest(ESC[4;38;5;149mZoneInfoPickleTest):
     208      module = c_zoneinfo
     209  
     210  
     211  class ESC[4;38;5;81mZoneInfoCacheTest(ESC[4;38;5;149mZoneInfoTestBase):
     212      module = py_zoneinfo
     213  
     214      @hypothesis.given(key=valid_keys())
     215      @add_key_examples
     216      def test_cache(self, key):
     217          zi_0 = self.klass(key)
     218          zi_1 = self.klass(key)
     219  
     220          self.assertIs(zi_0, zi_1)
     221  
     222      @hypothesis.given(key=valid_keys())
     223      @add_key_examples
     224      def test_no_cache(self, key):
     225          zi_0 = self.klass.no_cache(key)
     226          zi_1 = self.klass.no_cache(key)
     227  
     228          self.assertIsNot(zi_0, zi_1)
     229  
     230  
     231  class ESC[4;38;5;81mCZoneInfoCacheTest(ESC[4;38;5;149mZoneInfoCacheTest):
     232      klass = c_zoneinfo.ZoneInfo
     233  
     234  
     235  class ESC[4;38;5;81mPythonCConsistencyTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     236      """Tests that the C and Python versions do the same thing."""
     237  
     238      def _is_ambiguous(self, dt):
     239          return dt.replace(fold=not dt.fold).utcoffset() == dt.utcoffset()
     240  
     241      @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
     242      @hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
     243      @hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
     244      @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
     245      @hypothesis.example(dt=datetime.datetime(2020, 1, 1), key="Europe/Paris")
     246      @hypothesis.example(dt=datetime.datetime(2020, 6, 1), key="Europe/Paris")
     247      def test_same_str(self, dt, key):
     248          py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
     249          c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
     250  
     251          self.assertEqual(str(py_dt), str(c_dt))
     252  
     253      @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
     254      @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
     255      @hypothesis.example(dt=datetime.datetime(2020, 2, 5), key="America/New_York")
     256      @hypothesis.example(dt=datetime.datetime(2020, 8, 12), key="America/New_York")
     257      @hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Africa/Casablanca")
     258      @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="Europe/Paris")
     259      @hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Europe/Paris")
     260      @hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
     261      @hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
     262      def test_same_offsets_and_names(self, dt, key):
     263          py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
     264          c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
     265  
     266          self.assertEqual(py_dt.tzname(), c_dt.tzname())
     267          self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
     268          self.assertEqual(py_dt.dst(), c_dt.dst())
     269  
     270      @hypothesis.given(
     271          dt=hypothesis.strategies.datetimes(timezones=hypothesis.strategies.just(UTC)),
     272          key=valid_keys(),
     273      )
     274      @hypothesis.example(dt=MIN_UTC, key="Asia/Tokyo")
     275      @hypothesis.example(dt=MAX_UTC, key="Asia/Tokyo")
     276      @hypothesis.example(dt=MIN_UTC, key="America/New_York")
     277      @hypothesis.example(dt=MAX_UTC, key="America/New_York")
     278      @hypothesis.example(
     279          dt=datetime.datetime(2006, 10, 29, 5, 15, tzinfo=UTC),
     280          key="America/New_York",
     281      )
     282      def test_same_from_utc(self, dt, key):
     283          py_zi = py_zoneinfo.ZoneInfo(key)
     284          c_zi = c_zoneinfo.ZoneInfo(key)
     285  
     286          # Convert to UTC: This can overflow, but we just care about consistency
     287          py_overflow_exc = None
     288          c_overflow_exc = None
     289          try:
     290              py_dt = dt.astimezone(py_zi)
     291          except OverflowError as e:
     292              py_overflow_exc = e
     293  
     294          try:
     295              c_dt = dt.astimezone(c_zi)
     296          except OverflowError as e:
     297              c_overflow_exc = e
     298  
     299          if (py_overflow_exc is not None) != (c_overflow_exc is not None):
     300              raise py_overflow_exc or c_overflow_exc  # pragma: nocover
     301  
     302          if py_overflow_exc is not None:
     303              return  # Consistently raises the same exception
     304  
     305          # PEP 495 says that an inter-zone comparison between ambiguous
     306          # datetimes is always False.
     307          if py_dt != c_dt:
     308              self.assertEqual(
     309                  self._is_ambiguous(py_dt),
     310                  self._is_ambiguous(c_dt),
     311                  (py_dt, c_dt),
     312              )
     313  
     314          self.assertEqual(py_dt.tzname(), c_dt.tzname())
     315          self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
     316          self.assertEqual(py_dt.dst(), c_dt.dst())
     317  
     318      @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
     319      @hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
     320      @hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
     321      @hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
     322      @hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
     323      def test_same_to_utc(self, dt, key):
     324          py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
     325          c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
     326  
     327          # Convert from UTC: Overflow OK if it happens in both implementations
     328          py_overflow_exc = None
     329          c_overflow_exc = None
     330          try:
     331              py_utc = py_dt.astimezone(UTC)
     332          except OverflowError as e:
     333              py_overflow_exc = e
     334  
     335          try:
     336              c_utc = c_dt.astimezone(UTC)
     337          except OverflowError as e:
     338              c_overflow_exc = e
     339  
     340          if (py_overflow_exc is not None) != (c_overflow_exc is not None):
     341              raise py_overflow_exc or c_overflow_exc  # pragma: nocover
     342  
     343          if py_overflow_exc is not None:
     344              return  # Consistently raises the same exception
     345  
     346          self.assertEqual(py_utc, c_utc)
     347  
     348      @hypothesis.given(key=valid_keys())
     349      @add_key_examples
     350      def test_cross_module_pickle(self, key):
     351          py_zi = py_zoneinfo.ZoneInfo(key)
     352          c_zi = c_zoneinfo.ZoneInfo(key)
     353  
     354          with test_support.set_zoneinfo_module(py_zoneinfo):
     355              py_pkl = pickle.dumps(py_zi)
     356  
     357          with test_support.set_zoneinfo_module(c_zoneinfo):
     358              c_pkl = pickle.dumps(c_zi)
     359  
     360          with test_support.set_zoneinfo_module(c_zoneinfo):
     361              # Python → C
     362              py_to_c_zi = pickle.loads(py_pkl)
     363              self.assertIs(py_to_c_zi, c_zi)
     364  
     365          with test_support.set_zoneinfo_module(py_zoneinfo):
     366              # C → Python
     367              c_to_py_zi = pickle.loads(c_pkl)
     368              self.assertIs(c_to_py_zi, py_zi)