python (3.12.0)
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)