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_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 try: 1534 with OS_ENV_LOCK: 1535 old_env = os.environ.get(path_var, None) 1536 os.environ[path_var] = value 1537 yield 1538 finally: 1539 if old_env is None: 1540 del os.environ[path_var] 1541 else: 1542 os.environ[path_var] = old_env # pragma: nocover 1543 1544 def test_env_variable(self): 1545 """Tests that the environment variable works with reset_tzpath.""" 1546 new_paths = [ 1547 ("", []), 1548 ("/etc/zoneinfo", ["/etc/zoneinfo"]), 1549 (f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]), 1550 ] 1551 1552 for new_path_var, expected_result in new_paths: 1553 with self.python_tzpath_context(new_path_var): 1554 with self.subTest(tzpath=new_path_var): 1555 self.module.reset_tzpath() 1556 tzpath = self.module.TZPATH 1557 self.assertSequenceEqual(tzpath, expected_result) 1558 1559 def test_env_variable_relative_paths(self): 1560 test_cases = [ 1561 [("path/to/somewhere",), ()], 1562 [ 1563 ("/usr/share/zoneinfo", "path/to/somewhere",), 1564 ("/usr/share/zoneinfo",), 1565 ], 1566 [("../relative/path",), ()], 1567 [ 1568 ("/usr/share/zoneinfo", "../relative/path",), 1569 ("/usr/share/zoneinfo",), 1570 ], 1571 [("path/to/somewhere", "../relative/path",), ()], 1572 [ 1573 ( 1574 "/usr/share/zoneinfo", 1575 "path/to/somewhere", 1576 "../relative/path", 1577 ), 1578 ("/usr/share/zoneinfo",), 1579 ], 1580 ] 1581 1582 for input_paths, expected_paths in test_cases: 1583 path_var = os.pathsep.join(input_paths) 1584 with self.python_tzpath_context(path_var): 1585 with self.subTest("warning", path_var=path_var): 1586 # Note: Per PEP 615 the warning is implementation-defined 1587 # behavior, other implementations need not warn. 1588 with self.assertWarns(self.module.InvalidTZPathWarning): 1589 self.module.reset_tzpath() 1590 1591 tzpath = self.module.TZPATH 1592 with self.subTest("filtered", path_var=path_var): 1593 self.assertSequenceEqual(tzpath, expected_paths) 1594 1595 def test_reset_tzpath_kwarg(self): 1596 self.module.reset_tzpath(to=["/a/b/c"]) 1597 1598 self.assertSequenceEqual(self.module.TZPATH, ("/a/b/c",)) 1599 1600 def test_reset_tzpath_relative_paths(self): 1601 bad_values = [ 1602 ("path/to/somewhere",), 1603 ("/usr/share/zoneinfo", "path/to/somewhere",), 1604 ("../relative/path",), 1605 ("/usr/share/zoneinfo", "../relative/path",), 1606 ("path/to/somewhere", "../relative/path",), 1607 ("/usr/share/zoneinfo", "path/to/somewhere", "../relative/path",), 1608 ] 1609 for input_paths in bad_values: 1610 with self.subTest(input_paths=input_paths): 1611 with self.assertRaises(ValueError): 1612 self.module.reset_tzpath(to=input_paths) 1613 1614 def test_tzpath_type_error(self): 1615 bad_values = [ 1616 "/etc/zoneinfo:/usr/share/zoneinfo", 1617 b"/etc/zoneinfo:/usr/share/zoneinfo", 1618 0, 1619 ] 1620 1621 for bad_value in bad_values: 1622 with self.subTest(value=bad_value): 1623 with self.assertRaises(TypeError): 1624 self.module.reset_tzpath(bad_value) 1625 1626 def test_tzpath_attribute(self): 1627 tzpath_0 = ["/one", "/two"] 1628 tzpath_1 = ["/three"] 1629 1630 with self.tzpath_context(tzpath_0): 1631 query_0 = self.module.TZPATH 1632 1633 with self.tzpath_context(tzpath_1): 1634 query_1 = self.module.TZPATH 1635 1636 self.assertSequenceEqual(tzpath_0, query_0) 1637 self.assertSequenceEqual(tzpath_1, query_1) 1638 1639 1640class CTzPathTest(TzPathTest): 1641 module = c_zoneinfo 1642 1643 1644class TestModule(ZoneInfoTestBase): 1645 module = py_zoneinfo 1646 1647 @property 1648 def zoneinfo_data(self): 1649 return ZONEINFO_DATA 1650 1651 @cached_property 1652 def _UTC_bytes(self): 1653 zone_file = self.zoneinfo_data.path_from_key("UTC") 1654 with open(zone_file, "rb") as f: 1655 return f.read() 1656 1657 def touch_zone(self, key, tz_root): 1658 """Creates a valid TZif file at key under the zoneinfo root tz_root. 1659 1660 tz_root must exist, but all folders below that will be created. 1661 """ 1662 if not os.path.exists(tz_root): 1663 raise FileNotFoundError(f"{tz_root} does not exist.") 1664 1665 root_dir, *tail = key.rsplit("/", 1) 1666 if tail: # If there's no tail, then the first component isn't a dir 1667 os.makedirs(os.path.join(tz_root, root_dir), exist_ok=True) 1668 1669 zonefile_path = os.path.join(tz_root, key) 1670 with open(zonefile_path, "wb") as f: 1671 f.write(self._UTC_bytes) 1672 1673 def test_getattr_error(self): 1674 with self.assertRaises(AttributeError): 1675 self.module.NOATTRIBUTE 1676 1677 def test_dir_contains_all(self): 1678 """dir(self.module) should at least contain everything in __all__.""" 1679 module_all_set = set(self.module.__all__) 1680 module_dir_set = set(dir(self.module)) 1681 1682 difference = module_all_set - module_dir_set 1683 1684 self.assertFalse(difference) 1685 1686 def test_dir_unique(self): 1687 """Test that there are no duplicates in dir(self.module)""" 1688 module_dir = dir(self.module) 1689 module_unique = set(module_dir) 1690 1691 self.assertCountEqual(module_dir, module_unique) 1692 1693 def test_available_timezones(self): 1694 with self.tzpath_context([self.zoneinfo_data.tzpath]): 1695 self.assertTrue(self.zoneinfo_data.keys) # Sanity check 1696 1697 available_keys = self.module.available_timezones() 1698 zoneinfo_keys = set(self.zoneinfo_data.keys) 1699 1700 # If tzdata is not present, zoneinfo_keys == available_keys, 1701 # otherwise it should be a subset. 1702 union = zoneinfo_keys & available_keys 1703 self.assertEqual(zoneinfo_keys, union) 1704 1705 def test_available_timezones_weirdzone(self): 1706 with tempfile.TemporaryDirectory() as td: 1707 # Make a fictional zone at "Mars/Olympus_Mons" 1708 self.touch_zone("Mars/Olympus_Mons", td) 1709 1710 with self.tzpath_context([td]): 1711 available_keys = self.module.available_timezones() 1712 self.assertIn("Mars/Olympus_Mons", available_keys) 1713 1714 def test_folder_exclusions(self): 1715 expected = { 1716 "America/Los_Angeles", 1717 "America/Santiago", 1718 "America/Indiana/Indianapolis", 1719 "UTC", 1720 "Europe/Paris", 1721 "Europe/London", 1722 "Asia/Tokyo", 1723 "Australia/Sydney", 1724 } 1725 1726 base_tree = list(expected) 1727 posix_tree = [f"posix/{x}" for x in base_tree] 1728 right_tree = [f"right/{x}" for x in base_tree] 1729 1730 cases = [ 1731 ("base_tree", base_tree), 1732 ("base_and_posix", base_tree + posix_tree), 1733 ("base_and_right", base_tree + right_tree), 1734 ("all_trees", base_tree + right_tree + posix_tree), 1735 ] 1736 1737 with tempfile.TemporaryDirectory() as td: 1738 for case_name, tree in cases: 1739 tz_root = os.path.join(td, case_name) 1740 os.mkdir(tz_root) 1741 1742 for key in tree: 1743 self.touch_zone(key, tz_root) 1744 1745 with self.tzpath_context([tz_root]): 1746 with self.subTest(case_name): 1747 actual = self.module.available_timezones() 1748 self.assertEqual(actual, expected) 1749 1750 def test_exclude_posixrules(self): 1751 expected = { 1752 "America/New_York", 1753 "Europe/London", 1754 } 1755 1756 tree = list(expected) + ["posixrules"] 1757 1758 with tempfile.TemporaryDirectory() as td: 1759 for key in tree: 1760 self.touch_zone(key, td) 1761 1762 with self.tzpath_context([td]): 1763 actual = self.module.available_timezones() 1764 self.assertEqual(actual, expected) 1765 1766 1767class CTestModule(TestModule): 1768 module = c_zoneinfo 1769 1770 1771class ExtensionBuiltTest(unittest.TestCase): 1772 """Smoke test to ensure that the C and Python extensions are both tested. 1773 1774 Because the intention is for the Python and C versions of ZoneInfo to 1775 behave identically, these tests necessarily rely on implementation details, 1776 so the tests may need to be adjusted if the implementations change. Do not 1777 rely on these tests as an indication of stable properties of these classes. 1778 """ 1779 1780 def test_cache_location(self): 1781 # The pure Python version stores caches on attributes, but the C 1782 # extension stores them in C globals (at least for now) 1783 self.assertFalse(hasattr(c_zoneinfo.ZoneInfo, "_weak_cache")) 1784 self.assertTrue(hasattr(py_zoneinfo.ZoneInfo, "_weak_cache")) 1785 1786 def test_gc_tracked(self): 1787 # The pure Python version is tracked by the GC but (for now) the C 1788 # version is not. 1789 import gc 1790 1791 self.assertTrue(gc.is_tracked(py_zoneinfo.ZoneInfo)) 1792 self.assertFalse(gc.is_tracked(c_zoneinfo.ZoneInfo)) 1793 1794 1795@dataclasses.dataclass(frozen=True) 1796class ZoneOffset: 1797 tzname: str 1798 utcoffset: timedelta 1799 dst: timedelta = ZERO 1800 1801 1802@dataclasses.dataclass(frozen=True) 1803class ZoneTransition: 1804 transition: datetime 1805 offset_before: ZoneOffset 1806 offset_after: ZoneOffset 1807 1808 @property 1809 def transition_utc(self): 1810 return (self.transition - self.offset_before.utcoffset).replace( 1811 tzinfo=timezone.utc 1812 ) 1813 1814 @property 1815 def fold(self): 1816 """Whether this introduces a fold""" 1817 return self.offset_before.utcoffset > self.offset_after.utcoffset 1818 1819 @property 1820 def gap(self): 1821 """Whether this introduces a gap""" 1822 return self.offset_before.utcoffset < self.offset_after.utcoffset 1823 1824 @property 1825 def delta(self): 1826 return self.offset_after.utcoffset - self.offset_before.utcoffset 1827 1828 @property 1829 def anomaly_start(self): 1830 if self.fold: 1831 return self.transition + self.delta 1832 else: 1833 return self.transition 1834 1835 @property 1836 def anomaly_end(self): 1837 if not self.fold: 1838 return self.transition + self.delta 1839 else: 1840 return self.transition 1841 1842 1843class ZoneInfoData: 1844 def __init__(self, source_json, tzpath, v1=False): 1845 self.tzpath = pathlib.Path(tzpath) 1846 self.keys = [] 1847 self.v1 = v1 1848 self._populate_tzpath(source_json) 1849 1850 def path_from_key(self, key): 1851 return self.tzpath / key 1852 1853 def _populate_tzpath(self, source_json): 1854 with open(source_json, "rb") as f: 1855 zoneinfo_dict = json.load(f) 1856 1857 zoneinfo_data = zoneinfo_dict["data"] 1858 1859 for key, value in zoneinfo_data.items(): 1860 self.keys.append(key) 1861 raw_data = self._decode_text(value) 1862 1863 if self.v1: 1864 data = self._convert_to_v1(raw_data) 1865 else: 1866 data = raw_data 1867 1868 destination = self.path_from_key(key) 1869 destination.parent.mkdir(exist_ok=True, parents=True) 1870 with open(destination, "wb") as f: 1871 f.write(data) 1872 1873 def _decode_text(self, contents): 1874 raw_data = b"".join(map(str.encode, contents)) 1875 decoded = base64.b85decode(raw_data) 1876 1877 return lzma.decompress(decoded) 1878 1879 def _convert_to_v1(self, contents): 1880 assert contents[0:4] == b"TZif", "Invalid TZif data found!" 1881 version = int(contents[4:5]) 1882 1883 header_start = 4 + 16 1884 header_end = header_start + 24 # 6l == 24 bytes 1885 assert version >= 2, "Version 1 file found: no conversion necessary" 1886 isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt = struct.unpack( 1887 ">6l", contents[header_start:header_end] 1888 ) 1889 1890 file_size = ( 1891 timecnt * 5 1892 + typecnt * 6 1893 + charcnt 1894 + leapcnt * 8 1895 + isstdcnt 1896 + isutcnt 1897 ) 1898 file_size += header_end 1899 out = b"TZif" + b"\x00" + contents[5:file_size] 1900 1901 assert ( 1902 contents[file_size : (file_size + 4)] == b"TZif" 1903 ), "Version 2 file not truncated at Version 2 header" 1904 1905 return out 1906 1907 1908class ZoneDumpData: 1909 @classmethod 1910 def transition_keys(cls): 1911 return cls._get_zonedump().keys() 1912 1913 @classmethod 1914 def load_transition_examples(cls, key): 1915 return cls._get_zonedump()[key] 1916 1917 @classmethod 1918 def fixed_offset_zones(cls): 1919 if not cls._FIXED_OFFSET_ZONES: 1920 cls._populate_fixed_offsets() 1921 1922 return cls._FIXED_OFFSET_ZONES.items() 1923 1924 @classmethod 1925 def _get_zonedump(cls): 1926 if not cls._ZONEDUMP_DATA: 1927 cls._populate_zonedump_data() 1928 return cls._ZONEDUMP_DATA 1929 1930 @classmethod 1931 def _populate_fixed_offsets(cls): 1932 cls._FIXED_OFFSET_ZONES = { 1933 "UTC": ZoneOffset("UTC", ZERO, ZERO), 1934 } 1935 1936 @classmethod 1937 def _populate_zonedump_data(cls): 1938 def _Africa_Abidjan(): 1939 LMT = ZoneOffset("LMT", timedelta(seconds=-968)) 1940 GMT = ZoneOffset("GMT", ZERO) 1941 1942 return [ 1943 ZoneTransition(datetime(1912, 1, 1), LMT, GMT), 1944 ] 1945 1946 def _Africa_Casablanca(): 1947 P00_s = ZoneOffset("+00", ZERO, ZERO) 1948 P01_d = ZoneOffset("+01", ONE_H, ONE_H) 1949 P00_d = ZoneOffset("+00", ZERO, -ONE_H) 1950 P01_s = ZoneOffset("+01", ONE_H, ZERO) 1951 1952 return [ 1953 # Morocco sometimes pauses DST during Ramadan 1954 ZoneTransition(datetime(2018, 3, 25, 2), P00_s, P01_d), 1955 ZoneTransition(datetime(2018, 5, 13, 3), P01_d, P00_s), 1956 ZoneTransition(datetime(2018, 6, 17, 2), P00_s, P01_d), 1957 # On October 28th Morocco set standard time to +01, 1958 # with negative DST only during Ramadan 1959 ZoneTransition(datetime(2018, 10, 28, 3), P01_d, P01_s), 1960 ZoneTransition(datetime(2019, 5, 5, 3), P01_s, P00_d), 1961 ZoneTransition(datetime(2019, 6, 9, 2), P00_d, P01_s), 1962 ] 1963 1964 def _America_Los_Angeles(): 1965 LMT = ZoneOffset("LMT", timedelta(seconds=-28378), ZERO) 1966 PST = ZoneOffset("PST", timedelta(hours=-8), ZERO) 1967 PDT = ZoneOffset("PDT", timedelta(hours=-7), ONE_H) 1968 PWT = ZoneOffset("PWT", timedelta(hours=-7), ONE_H) 1969 PPT = ZoneOffset("PPT", timedelta(hours=-7), ONE_H) 1970 1971 return [ 1972 ZoneTransition(datetime(1883, 11, 18, 12, 7, 2), LMT, PST), 1973 ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT), 1974 ZoneTransition(datetime(1918, 3, 31, 2), PST, PDT), 1975 ZoneTransition(datetime(1918, 10, 27, 2), PDT, PST), 1976 # Transition to Pacific War Time 1977 ZoneTransition(datetime(1942, 2, 9, 2), PST, PWT), 1978 # Transition from Pacific War Time to Pacific Peace Time 1979 ZoneTransition(datetime(1945, 8, 14, 16), PWT, PPT), 1980 ZoneTransition(datetime(1945, 9, 30, 2), PPT, PST), 1981 ZoneTransition(datetime(2015, 3, 8, 2), PST, PDT), 1982 ZoneTransition(datetime(2015, 11, 1, 2), PDT, PST), 1983 # After 2038: Rules continue indefinitely 1984 ZoneTransition(datetime(2450, 3, 13, 2), PST, PDT), 1985 ZoneTransition(datetime(2450, 11, 6, 2), PDT, PST), 1986 ] 1987 1988 def _America_Santiago(): 1989 LMT = ZoneOffset("LMT", timedelta(seconds=-16966), ZERO) 1990 SMT = ZoneOffset("SMT", timedelta(seconds=-16966), ZERO) 1991 N05 = ZoneOffset("-05", timedelta(seconds=-18000), ZERO) 1992 N04 = ZoneOffset("-04", timedelta(seconds=-14400), ZERO) 1993 N03 = ZoneOffset("-03", timedelta(seconds=-10800), ONE_H) 1994 1995 return [ 1996 ZoneTransition(datetime(1890, 1, 1), LMT, SMT), 1997 ZoneTransition(datetime(1910, 1, 10), SMT, N05), 1998 ZoneTransition(datetime(1916, 7, 1), N05, SMT), 1999 ZoneTransition(datetime(2008, 3, 30), N03, N04), 2000 ZoneTransition(datetime(2008, 10, 12), N04, N03), 2001 ZoneTransition(datetime(2040, 4, 8), N03, N04), 2002 ZoneTransition(datetime(2040, 9, 2), N04, N03), 2003 ] 2004 2005 def _Asia_Tokyo(): 2006 JST = ZoneOffset("JST", timedelta(seconds=32400), ZERO) 2007 JDT = ZoneOffset("JDT", timedelta(seconds=36000), ONE_H) 2008 2009 # Japan had DST from 1948 to 1951, and it was unusual in that 2010 # the transition from DST to STD occurred at 25:00, and is 2011 # denominated as such in the time zone database 2012 return [ 2013 ZoneTransition(datetime(1948, 5, 2), JST, JDT), 2014 ZoneTransition(datetime(1948, 9, 12, 1), JDT, JST), 2015 ZoneTransition(datetime(1951, 9, 9, 1), JDT, JST), 2016 ] 2017 2018 def _Australia_Sydney(): 2019 LMT = ZoneOffset("LMT", timedelta(seconds=36292), ZERO) 2020 AEST = ZoneOffset("AEST", timedelta(seconds=36000), ZERO) 2021 AEDT = ZoneOffset("AEDT", timedelta(seconds=39600), ONE_H) 2022 2023 return [ 2024 ZoneTransition(datetime(1895, 2, 1), LMT, AEST), 2025 ZoneTransition(datetime(1917, 1, 1, 0, 1), AEST, AEDT), 2026 ZoneTransition(datetime(1917, 3, 25, 2), AEDT, AEST), 2027 ZoneTransition(datetime(2012, 4, 1, 3), AEDT, AEST), 2028 ZoneTransition(datetime(2012, 10, 7, 2), AEST, AEDT), 2029 ZoneTransition(datetime(2040, 4, 1, 3), AEDT, AEST), 2030 ZoneTransition(datetime(2040, 10, 7, 2), AEST, AEDT), 2031 ] 2032 2033 def _Europe_Dublin(): 2034 LMT = ZoneOffset("LMT", timedelta(seconds=-1500), ZERO) 2035 DMT = ZoneOffset("DMT", timedelta(seconds=-1521), ZERO) 2036 IST_0 = ZoneOffset("IST", timedelta(seconds=2079), ONE_H) 2037 GMT_0 = ZoneOffset("GMT", ZERO, ZERO) 2038 BST = ZoneOffset("BST", ONE_H, ONE_H) 2039 GMT_1 = ZoneOffset("GMT", ZERO, -ONE_H) 2040 IST_1 = ZoneOffset("IST", ONE_H, ZERO) 2041 2042 return [ 2043 ZoneTransition(datetime(1880, 8, 2, 0), LMT, DMT), 2044 ZoneTransition(datetime(1916, 5, 21, 2), DMT, IST_0), 2045 ZoneTransition(datetime(1916, 10, 1, 3), IST_0, GMT_0), 2046 ZoneTransition(datetime(1917, 4, 8, 2), GMT_0, BST), 2047 ZoneTransition(datetime(2016, 3, 27, 1), GMT_1, IST_1), 2048 ZoneTransition(datetime(2016, 10, 30, 2), IST_1, GMT_1), 2049 ZoneTransition(datetime(2487, 3, 30, 1), GMT_1, IST_1), 2050 ZoneTransition(datetime(2487, 10, 26, 2), IST_1, GMT_1), 2051 ] 2052 2053 def _Europe_Lisbon(): 2054 WET = ZoneOffset("WET", ZERO, ZERO) 2055 WEST = ZoneOffset("WEST", ONE_H, ONE_H) 2056 CET = ZoneOffset("CET", ONE_H, ZERO) 2057 CEST = ZoneOffset("CEST", timedelta(seconds=7200), ONE_H) 2058 2059 return [ 2060 ZoneTransition(datetime(1992, 3, 29, 1), WET, WEST), 2061 ZoneTransition(datetime(1992, 9, 27, 2), WEST, CET), 2062 ZoneTransition(datetime(1993, 3, 28, 2), CET, CEST), 2063 ZoneTransition(datetime(1993, 9, 26, 3), CEST, CET), 2064 ZoneTransition(datetime(1996, 3, 31, 2), CET, WEST), 2065 ZoneTransition(datetime(1996, 10, 27, 2), WEST, WET), 2066 ] 2067 2068 def _Europe_London(): 2069 LMT = ZoneOffset("LMT", timedelta(seconds=-75), ZERO) 2070 GMT = ZoneOffset("GMT", ZERO, ZERO) 2071 BST = ZoneOffset("BST", ONE_H, ONE_H) 2072 2073 return [ 2074 ZoneTransition(datetime(1847, 12, 1), LMT, GMT), 2075 ZoneTransition(datetime(2005, 3, 27, 1), GMT, BST), 2076 ZoneTransition(datetime(2005, 10, 30, 2), BST, GMT), 2077 ZoneTransition(datetime(2043, 3, 29, 1), GMT, BST), 2078 ZoneTransition(datetime(2043, 10, 25, 2), BST, GMT), 2079 ] 2080 2081 def _Pacific_Kiritimati(): 2082 LMT = ZoneOffset("LMT", timedelta(seconds=-37760), ZERO) 2083 N1040 = ZoneOffset("-1040", timedelta(seconds=-38400), ZERO) 2084 N10 = ZoneOffset("-10", timedelta(seconds=-36000), ZERO) 2085 P14 = ZoneOffset("+14", timedelta(seconds=50400), ZERO) 2086 2087 # This is literally every transition in Christmas Island history 2088 return [ 2089 ZoneTransition(datetime(1901, 1, 1), LMT, N1040), 2090 ZoneTransition(datetime(1979, 10, 1), N1040, N10), 2091 # They skipped December 31, 1994 2092 ZoneTransition(datetime(1994, 12, 31), N10, P14), 2093 ] 2094 2095 cls._ZONEDUMP_DATA = { 2096 "Africa/Abidjan": _Africa_Abidjan(), 2097 "Africa/Casablanca": _Africa_Casablanca(), 2098 "America/Los_Angeles": _America_Los_Angeles(), 2099 "America/Santiago": _America_Santiago(), 2100 "Australia/Sydney": _Australia_Sydney(), 2101 "Asia/Tokyo": _Asia_Tokyo(), 2102 "Europe/Dublin": _Europe_Dublin(), 2103 "Europe/Lisbon": _Europe_Lisbon(), 2104 "Europe/London": _Europe_London(), 2105 "Pacific/Kiritimati": _Pacific_Kiritimati(), 2106 } 2107 2108 _ZONEDUMP_DATA = None 2109 _FIXED_OFFSET_ZONES = None 2110