From 11755f3cbee3998be27ff891fc26a90bb162a501 Mon Sep 17 00:00:00 2001 From: emme1t <149944796+emme1t@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:47:27 -0700 Subject: [PATCH 1/2] Restore hashing for named timezones --- src/pendulum/tz/timezone.py | 3 +++ tests/tz/test_timezone.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/pendulum/tz/timezone.py b/src/pendulum/tz/timezone.py index e46e13df1..8b1e55690 100644 --- a/src/pendulum/tz/timezone.py +++ b/src/pendulum/tz/timezone.py @@ -69,6 +69,9 @@ def __new__(cls, key: str) -> Self: def __eq__(self, other: object) -> bool: return isinstance(other, Timezone) and self.key == other.key + def __hash__(self) -> int: + return hash(self.key) + @property def name(self) -> str: return self.key diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py index 3f0901681..0bff069fa 100644 --- a/tests/tz/test_timezone.py +++ b/tests/tz/test_timezone.py @@ -47,6 +47,29 @@ def test_equality(): assert timezone("Europe/Paris") != timezone("Europe/Berlin") +@pytest.mark.parametrize("name", ["UTC", "Europe/Paris", "America/New_York"]) +def test_equal_timezones_are_interchangeable_mapping_keys(name): + first = pendulum.Timezone.no_cache(name) + second = pendulum.Timezone.no_cache(name) + + assert first is not second + assert first == second + assert hash(first) == hash(second) + assert {first: "value"}[second] == "value" + assert len({first, second}) == 1 + + +def test_distinct_timezones_are_distinct_mapping_keys(): + paris = timezone("Europe/Paris") + berlin = timezone("Europe/Berlin") + + values = {paris: "Paris", berlin: "Berlin"} + + assert len(values) == 2 + assert values[pendulum.Timezone.no_cache("Europe/Paris")] == "Paris" + assert values[pendulum.Timezone.no_cache("Europe/Berlin")] == "Berlin" + + def test_skipped_time_with_pre_rule(): dt = datetime(2013, 3, 31, 2, 30, 45, 123456, fold=0) tz = timezone("Europe/Paris") From b62344626170187ad0a6e4ef6a4dcb174c533b36 Mon Sep 17 00:00:00 2001 From: emme1t <149944796+emme1t@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:00:24 -0700 Subject: [PATCH 2/2] Qualify timezone hashes and cover subclass equality --- src/pendulum/tz/timezone.py | 2 +- tests/tz/test_timezone.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pendulum/tz/timezone.py b/src/pendulum/tz/timezone.py index 8b1e55690..7ff0fd2d9 100644 --- a/src/pendulum/tz/timezone.py +++ b/src/pendulum/tz/timezone.py @@ -70,7 +70,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, Timezone) and self.key == other.key def __hash__(self) -> int: - return hash(self.key) + return hash((Timezone, self.key)) @property def name(self) -> str: diff --git a/tests/tz/test_timezone.py b/tests/tz/test_timezone.py index 0bff069fa..d92273576 100644 --- a/tests/tz/test_timezone.py +++ b/tests/tz/test_timezone.py @@ -59,6 +59,21 @@ def test_equal_timezones_are_interchangeable_mapping_keys(name): assert len({first, second}) == 1 +@pytest.mark.parametrize("name", ["UTC", "Europe/Paris", "America/New_York"]) +def test_timezone_subclasses_are_interchangeable_mapping_keys(name): + class CustomTimezone(pendulum.Timezone): + pass + + base = pendulum.Timezone.no_cache(name) + derived = CustomTimezone.no_cache(name) + + assert base == derived + assert derived == base + assert hash(base) == hash(derived) + assert {base: "value"}[derived] == "value" + assert len({base, derived}) == 1 + + def test_distinct_timezones_are_distinct_mapping_keys(): paris = timezone("Europe/Paris") berlin = timezone("Europe/Berlin")