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