diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index a5788cdbf..832d160de 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -279,7 +279,13 @@ class ForwardRef: # because dictionaries are not hashable. and self.__globals__ is other.__globals__ and self.__forward_is_class__ == other.__forward_is_class__ - and self.__cell__ == other.__cell__ + # Two separate cells are always considered unequal in forward refs. + and ( + {name: id(cell) for name, cell in self.__cell__.items()} + == {name: id(cell) for name, cell in other.__cell__.items()} + if isinstance(self.__cell__, dict) and isinstance(other.__cell__, dict) + else self.__cell__ is other.__cell__ + ) and self.__owner__ == other.__owner__ and ( (tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None) == @@ -293,7 +299,10 @@ class ForwardRef: self.__forward_module__, id(self.__globals__), # dictionaries are not hashable, so hash by identity self.__forward_is_class__, - tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__, + ( # cells are not hashable as well + 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, )) @@ -1096,7 +1105,7 @@ def _rewrite_star_unpack(arg): """If the given argument annotation expression is a star unpack e.g. `'*Ts'` rewrite it to a valid expression. """ - if arg.startswith("*"): + if arg.lstrip().startswith("*"): return f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] else: return arg diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 8208d0e9c..e89d6c0b1 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -8,6 +8,7 @@ import functools import itertools import pickle from string.templatelib import Template, Interpolation +import types import typing import sys import unittest @@ -120,8 +121,8 @@ class TestForwardRefFormat(unittest.TestCase): beta: +some, gamma: some < obj, delta: some | {obj: module}, - epsilon: some | {obj, module}, - zeta: some | [obj], + epsilon: some | {obj}, + zeta: some | [obj, module], eta: some | (), ): pass @@ -157,11 +158,11 @@ class TestForwardRefFormat(unittest.TestCase): epsilon_anno = anno["epsilon"] self.assertIsInstance(epsilon_anno, ForwardRef) - self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj, module}", owner=f)) + self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) zeta_anno = anno["zeta"] self.assertIsInstance(zeta_anno, ForwardRef) - self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj]", owner=f)) + self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) eta_anno = anno["eta"] self.assertIsInstance(eta_anno, ForwardRef) @@ -885,6 +886,9 @@ class TestGetAnnotations(unittest.TestCase): def f(*args: "*tuple[int, ...]"): ... self.assertEqual(get_annotations(f, eval_str=True), {'args': (*tuple[int, ...],)[0]}) + def f(*args: " *tuple[int, ...]"): ... + self.assertEqual(get_annotations(f, eval_str=True), + {'args': (*tuple[int, ...],)[0]}) def test_stringized_annotations_on_wrapper(self): @@ -1859,6 +1863,39 @@ class TestForwardRefClass(unittest.TestCase): self.assertNotEqual(hash(c3), hash(c4)) self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__))) + def test_forward_equality_and_hash_with_cells(self): + """Regression test for GH-143831.""" + class A: + def one(_) -> C1: + """One cell.""" + + one_f = ForwardRef("C1", owner=one) + one_f_ga1 = get_annotations(one, format=Format.FORWARDREF)["return"] + one_f_ga2 = get_annotations(one, format=Format.FORWARDREF)["return"] + self.assertIsInstance(one_f_ga1.__cell__, types.CellType) + self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__) + + def two(_) -> C1 | C2: + """Two cells.""" + + two_f_ga1 = get_annotations(two, format=Format.FORWARDREF)["return"] + two_f_ga2 = get_annotations(two, format=Format.FORWARDREF)["return"] + self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__) + self.assertIsInstance(two_f_ga1.__cell__, dict) + self.assertIsInstance(two_f_ga2.__cell__, dict) + + type C1 = None + type C2 = None + + self.assertNotEqual(A.one_f, A.one_f_ga1) + self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1)) + + self.assertEqual(A.one_f_ga1, A.one_f_ga2) + self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2)) + + 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_namespace(self): def namespace1(): a = ForwardRef("A")