From fba5efa2a04871b845403443f85b18a20d303e3a Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Tue, 22 Apr 2025 16:53:28 +0100 Subject: [PATCH 1/4] Add a test for union forward references --- Lib/test/test_annotationlib.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 0890be529a7e52..e5dd8dc20e26b0 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -936,6 +936,28 @@ def __call__(self): annotationlib.get_annotations(obj, format=format), {} ) + def test_union_forwardref(self): + # Test unions with '|' syntax equal unions with typing.Union[] with forwardrefs + class UnionForwardrefs: + pipe: str | undefined + union: Union[str, undefined] + + annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) + + match = ( + str, + support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) + ) + + self.assertEqual( + typing.get_args(annos["pipe"]), + typing.get_args(annos["union"]) + ) + + self.assertEqual(typing.get_args(annos["pipe"]), match) + self.assertEqual(typing.get_args(annos["union"]), match) + + def test_pep695_generic_class_with_future_annotations(self): ann_module695 = inspect_stringized_annotations_pep695 A_annotations = annotationlib.get_annotations(ann_module695.A, eval_str=True) From d8109538837278622b4f3809d5ae51af942ad9ac Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Tue, 22 Apr 2025 16:54:52 +0100 Subject: [PATCH 2/4] Make stringifiers create unions if create_unions is True --- Lib/annotationlib.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 971f636f9714d7..c095efd6f3626a 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -392,12 +392,19 @@ def binop(self, other): __mod__ = _make_binop(ast.Mod()) __lshift__ = _make_binop(ast.LShift()) __rshift__ = _make_binop(ast.RShift()) - __or__ = _make_binop(ast.BitOr()) __xor__ = _make_binop(ast.BitXor()) __and__ = _make_binop(ast.BitAnd()) __floordiv__ = _make_binop(ast.FloorDiv()) __pow__ = _make_binop(ast.Pow()) + def __or__(self, other): + if self.__stringifier_dict__.create_unions: + return types.UnionType[self, other] + + return self.__make_new( + ast.BinOp(self.__get_ast(), ast.BitOr(), self.__convert_to_ast(other)) + ) + del _make_binop def _make_rbinop(op: ast.AST): @@ -416,12 +423,19 @@ def rbinop(self, other): __rmod__ = _make_rbinop(ast.Mod()) __rlshift__ = _make_rbinop(ast.LShift()) __rrshift__ = _make_rbinop(ast.RShift()) - __ror__ = _make_rbinop(ast.BitOr()) __rxor__ = _make_rbinop(ast.BitXor()) __rand__ = _make_rbinop(ast.BitAnd()) __rfloordiv__ = _make_rbinop(ast.FloorDiv()) __rpow__ = _make_rbinop(ast.Pow()) + def __ror__(self, other): + if self.__stringifier_dict__.create_unions: + return types.UnionType[other, self] + + return self.__make_new( + ast.BinOp(self.__convert_to_ast(other), ast.BitOr(), self.__get_ast()) + ) + del _make_rbinop def _make_compare(op): @@ -459,12 +473,13 @@ def unary_op(self): class _StringifierDict(dict): - def __init__(self, namespace, globals=None, owner=None, is_class=False): + def __init__(self, namespace, globals=None, owner=None, is_class=False, create_unions=False): super().__init__(namespace) self.namespace = namespace self.globals = globals self.owner = owner self.is_class = is_class + self.create_unions = create_unions self.stringifiers = [] def __missing__(self, key): @@ -569,7 +584,13 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # that returns a bool and an defined set of attributes. namespace = {**annotate.__builtins__, **annotate.__globals__} is_class = isinstance(owner, type) - globals = _StringifierDict(namespace, annotate.__globals__, owner, is_class) + globals = _StringifierDict( + namespace, + annotate.__globals__, + owner, + is_class, + create_unions=True + ) if annotate.__closure__: freevars = annotate.__code__.co_freevars new_closure = [] From 1908a4a6ca0a70e45128237a07f0e307ff283eef Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Tue, 22 Apr 2025 16:59:03 +0100 Subject: [PATCH 3/4] modify broken test, move test to forwardref format group --- Lib/test/test_annotationlib.py | 50 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index e5dd8dc20e26b0..fba1f11197c87d 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -115,8 +115,11 @@ def f( self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) alpha_anno = anno["alpha"] - self.assertIsInstance(alpha_anno, ForwardRef) - self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) + self.assertIsInstance(alpha_anno, Union) + self.assertEqual( + typing.get_args(alpha_anno), + (support.EqualToForwardRef("some", owner=f), support.EqualToForwardRef("obj", owner=f)) + ) beta_anno = anno["beta"] self.assertIsInstance(beta_anno, ForwardRef) @@ -126,6 +129,27 @@ def f( self.assertIsInstance(gamma_anno, ForwardRef) self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) + def test_partially_nonexistent_union(self): + # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs + class UnionForwardrefs: + pipe: str | undefined + union: Union[str, undefined] + + annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) + + match = ( + str, + support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) + ) + + self.assertEqual( + typing.get_args(annos["pipe"]), + typing.get_args(annos["union"]) + ) + + self.assertEqual(typing.get_args(annos["pipe"]), match) + self.assertEqual(typing.get_args(annos["union"]), match) + class TestSourceFormat(unittest.TestCase): def test_closure(self): @@ -936,28 +960,6 @@ def __call__(self): annotationlib.get_annotations(obj, format=format), {} ) - def test_union_forwardref(self): - # Test unions with '|' syntax equal unions with typing.Union[] with forwardrefs - class UnionForwardrefs: - pipe: str | undefined - union: Union[str, undefined] - - annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) - - match = ( - str, - support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) - ) - - self.assertEqual( - typing.get_args(annos["pipe"]), - typing.get_args(annos["union"]) - ) - - self.assertEqual(typing.get_args(annos["pipe"]), match) - self.assertEqual(typing.get_args(annos["union"]), match) - - def test_pep695_generic_class_with_future_annotations(self): ann_module695 = inspect_stringized_annotations_pep695 A_annotations = annotationlib.get_annotations(ann_module695.A, eval_str=True) From 97abdc7f44f17d84d248e4f762d5499e5091cbb7 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Tue, 22 Apr 2025 18:21:20 +0100 Subject: [PATCH 4/4] Apparently trim trailing whitespace was turned off --- Lib/annotationlib.py | 14 +++++++------- Lib/test/test_annotationlib.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index c095efd6f3626a..b4cecb41d13c46 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -400,7 +400,7 @@ def binop(self, other): def __or__(self, other): if self.__stringifier_dict__.create_unions: return types.UnionType[self, other] - + return self.__make_new( ast.BinOp(self.__get_ast(), ast.BitOr(), self.__convert_to_ast(other)) ) @@ -431,11 +431,11 @@ def rbinop(self, other): def __ror__(self, other): if self.__stringifier_dict__.create_unions: return types.UnionType[other, self] - + return self.__make_new( ast.BinOp(self.__convert_to_ast(other), ast.BitOr(), self.__get_ast()) ) - + del _make_rbinop def _make_compare(op): @@ -585,10 +585,10 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): namespace = {**annotate.__builtins__, **annotate.__globals__} is_class = isinstance(owner, type) globals = _StringifierDict( - namespace, - annotate.__globals__, - owner, - is_class, + namespace, + annotate.__globals__, + owner, + is_class, create_unions=True ) if annotate.__closure__: diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index fba1f11197c87d..6f17c85659c34e 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -117,7 +117,7 @@ def f( alpha_anno = anno["alpha"] self.assertIsInstance(alpha_anno, Union) self.assertEqual( - typing.get_args(alpha_anno), + typing.get_args(alpha_anno), (support.EqualToForwardRef("some", owner=f), support.EqualToForwardRef("obj", owner=f)) )