Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,15 @@ def __eq__(self, other):
if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict)
else self.__cell__ is other.__cell__
)
and self.__owner__ == other.__owner__
# Owners may be unhashable; __hash__ uses id(), so compare by "is".
and self.__owner__ is other.__owner__
# Compare extra-name values by identity (see __hash__).
and (
(tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) ==
(tuple(sorted(other.__extra_names__.items())) if other.__extra_names__ else None)
{n: id(v) for n, v in self.__extra_names__.items()}
if self.__extra_names__ else None
) == (
{n: id(v) for n, v in other.__extra_names__.items()}
if other.__extra_names__ else None
)
)

Expand All @@ -300,8 +305,13 @@ def __hash__(self):
tuple(sorted([(name, id(cell)) for name, cell in self.__cell__.items()]))
if isinstance(self.__cell__, dict) else id(self.__cell__),
),
self.__owner__,
tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
id(self.__owner__), # owners may be unhashable, so hash by identity
( # extra-name values may be unhashable, so hash by identity
tuple(sorted(
(n, id(v)) for n, v in self.__extra_names__.items()
))
if self.__extra_names__ else None
),
))

def __or__(self, other):
Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,49 @@ def two(_) -> C1 | C2:
self.assertEqual(A.two_f_ga1, A.two_f_ga2)
self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2))

def test_forward_equality_and_hash_with_extra_names(self):
"""Regression test for the __extra_names__ sibling of GH-143831."""
class Unhashable:
__hash__ = None

# An unhashable value referenced in an annotation is kept in the
# forward ref's __extra_names__.
ns = support.run_code(
"def f(a: undefined | obj): pass",
extra_names={"obj": Unhashable()},
)
fr1 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"]
fr2 = get_annotations(ns["f"], format=Format.FORWARDREF)["a"]
self.assertIsInstance(fr1.__extra_names__, dict)

self.assertEqual(fr1, fr2)
self.assertEqual(hash(fr1), hash(fr2))
self.assertEqual(len({fr1, fr2}), 1)

# Forward refs with different extra-name values are unequal.
ns2 = support.run_code(
"def g(a: undefined | obj): pass",
extra_names={"obj": Unhashable()},
)
fr3 = get_annotations(ns2["g"], format=Format.FORWARDREF)["a"]
self.assertNotEqual(fr1, fr3)
self.assertEqual(len({fr1, fr2, fr3}), 2)

def test_forward_equality_and_hash_with_unhashable_owner(self):
"""Regression test for an unhashable __owner__ (GH-143831 sibling)."""
class MetaNoHash(type):
__hash__ = None

class D(metaclass=MetaNoHash):
x: undefined

fr1 = get_annotations(D, format=Format.FORWARDREF)["x"]
fr2 = get_annotations(D, format=Format.FORWARDREF)["x"]
self.assertIs(fr1.__owner__, D)
self.assertEqual(fr1, fr2)
self.assertEqual(hash(fr1), hash(fr2))
self.assertEqual(len({fr1, fr2}), 1)

def test_forward_equality_namespace(self):
def namespace1():
a = ForwardRef("A")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`hash` raising :exc:`TypeError` on an
:class:`annotationlib.ForwardRef` that holds an unhashable value.
Patch by tonghuaroot.
Loading