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