From 6ee2f885775bee3d2e61894ce70ac405707932f9 Mon Sep 17 00:00:00 2001 From: Trung Minh Do Date: Sat, 25 Jul 2026 23:45:58 +0200 Subject: [PATCH] Fix LangStr hash/eq contract violation (#255) Change __hash__ to return hash(str(self)) so that LangStr instances equal to a plain str also share the same hash. This restores the Python data model invariant a == b => hash(a) == hash(b). Previously __hash__ returned hash((text, lang)) which always included the language code, while __eq__ fell back to value-only comparison when compared with a plain str (no .lang attribute). This meant LangStr instances could not be found in str-keyed dicts or sets. Two LangStrs with the same text but different languages now share a hash bucket, which is fine because __eq__ still separates them. --- rigour/langs/text.py | 2 +- tests/langs/test_text.py | 47 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/rigour/langs/text.py b/rigour/langs/text.py index 4e2be37e..19734a18 100644 --- a/rigour/langs/text.py +++ b/rigour/langs/text.py @@ -25,7 +25,7 @@ def __repr__(self) -> str: return super().__repr__() def __hash__(self) -> int: - return hash((super().__str__(), self.lang)) + return hash(str(self)) def __eq__(self, value: object) -> bool: try: diff --git a/tests/langs/test_text.py b/tests/langs/test_text.py index 078ac61d..6223edd5 100644 --- a/tests/langs/test_text.py +++ b/tests/langs/test_text.py @@ -20,8 +20,53 @@ def test_langstr(): assert repr(text) == '"Hello"@eng' assert "Hello" == text - assert hash(text) == hash(("Hello", "eng")) + assert hash(text) == hash("Hello") assert text != LangStr("Hello", lang="deu") with pytest.raises(ValueError): LangStr("Hello", lang="invalid") + + +def test_langstr_hash_eq_contract(): + """LangStr must satisfy a == b => hash(a) == hash(b) with plain str (#255).""" + a = LangStr("foo", "eng") + assert a == "foo" + assert hash(a) == hash("foo") + assert "foo" == a + assert hash("foo") == hash(a) + + +def test_langstr_str_keyed_dict(): + """LangStr lookups in str-keyed dicts must work after the hash fix (#255).""" + a = LangStr("foo", "eng") + d = {"foo": 1} + assert d.get(a) == 1 + assert a in d + + +def test_langstr_str_keyed_set(): + """LangStr instances equal to a plain str must deduplicate in sets (#255).""" + a = LangStr("foo", "eng") + s = {"foo", a} + assert len(s) == 1 + assert "foo" in s + + +def test_langstr_lang_differing(): + """Two LangStrs with same text but different lang share a hash bucket. + + They are NOT equal (lang differs), but they must hash to the same value + since both compare equal to the same plain str — the hash/eq contract + requires hash(a) == hash(b) whenever a == c and b == c for a common c. + """ + a = LangStr("foo", "eng") + b = LangStr("foo", "deu") + assert a != b + assert hash(a) == hash(b) + + +def test_langstr_none_lang(): + """LangStr with None lang still satisfies the hash/eq contract.""" + a = LangStr("foo", None) + assert a == "foo" + assert hash(a) == hash("foo")