From a03009cb9520f6c6b4681a9a5ff57fa05b003e8f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Feb 2022 11:23:04 +0300 Subject: [PATCH 1/3] bpo-46571: improve `typing.no_type_check` to skip foreign objects --- Doc/library/typing.rst | 4 +- Lib/test/test_typing.py | 64 +++++++++++++++++++ Lib/typing.py | 12 ++-- .../2022-02-01-11-21-34.bpo-46571.L40xUJ.rst | 2 + 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index cdfd403a34ef91..62a6e0c38d3cf6 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1999,8 +1999,8 @@ Functions and decorators Decorator to indicate that annotations are not type hints. This works as class or function :term:`decorator`. With a class, it - applies recursively to all methods defined in that class (but not - to methods defined in its superclasses or subclasses). + applies recursively to all methods and classes defined in that class + (but not to methods defined in its superclasses or subclasses). This mutates the function(s) in place. diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8449affd03a768..aa29c48654cefa 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2965,9 +2965,73 @@ def meth(self, x: int): ... @no_type_check class D(C): c = C + # verify that @no_type_check never affects bases self.assertEqual(get_type_hints(C.meth), {'x': int}) + # and never child classes: + class Child(D): + def foo(self, x: int): ... + + self.assertEqual(get_type_hints(Child.foo), {'x': int}) + + def test_no_type_check_nested_types(self): + # See https://bugs.python.org/issue46571 + class Other: + o: int + class B: # Has the same `__name__`` as `A.B` and different `__qualname__` + o: int + @no_type_check + class A: + a: int + class B: + b: int + class C: + c: int + class D: + d: int + + Other = Other + + for klass in [A, A.B, A.B.C, A.D]: + with self.subTest(klass=klass): + self.assertTrue(klass.__no_type_check__) + self.assertEqual(get_type_hints(klass), {}) + + for not_modified in [Other, B]: + with self.subTest(not_modified=not_modified): + with self.assertRaises(AttributeError): + not_modified.__no_type_check__ + self.assertNotEqual(get_type_hints(not_modified), {}) + + def test_no_type_check_foreign_functions(self): + # We should not modify this function: + def some(*args: int) -> int: + ... + + @no_type_check + class A: + some_alias = some + some_class = classmethod(some) + some_static = staticmethod(some) + + with self.assertRaises(AttributeError): + some.__no_type_check__ + + def test_no_type_check_lambda(self): + @no_type_check + class A: + # Corner case: `lambda` is both an assignment and a function: + bar: Callable[[int], int] = lambda arg: arg + + self.assertTrue(A.bar.__no_type_check__) + self.assertEqual(get_type_hints(A.bar), {}) + + def test_no_type_check_TypeError(self): + # This simply should not fail with + # `TypeError: can't set attributes of built-in/extension type 'dict'` + no_type_check(dict) + def test_no_type_check_forward_ref_as_string(self): class C: foo: typing.ClassVar[int] = 7 diff --git a/Lib/typing.py b/Lib/typing.py index dac9c6c4f87cfe..30d57e4e277b48 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1959,11 +1959,13 @@ def no_type_check(arg): This mutates the function(s) or class(es) in place. """ if isinstance(arg, type): - arg_attrs = arg.__dict__.copy() - for attr, val in arg.__dict__.items(): - if val in arg.__bases__ + (arg,): - arg_attrs.pop(attr) - for obj in arg_attrs.values(): + for obj in arg.__dict__.values(): + if (hasattr(obj, '__qualname__') + and obj.__qualname__ != f'{arg.__qualname__}.{obj.__name__}'): + # We only modify objects that are defined in this type directly. + # If classes / methods are nested in multiple layers, + # we will modify them when processing their direct holders. + continue if isinstance(obj, types.FunctionType): obj.__no_type_check__ = True if isinstance(obj, type): diff --git a/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst b/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst new file mode 100644 index 00000000000000..7a6e0668e18acf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst @@ -0,0 +1,2 @@ +Improve :func:`typing.no_type_check` to not modify foreign functions and +types. From 964dc69416b729505441b923890d84ae324e4d8e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 2 Feb 2022 14:11:53 +0300 Subject: [PATCH 2/3] Address review, add `@classmethod` support --- Lib/test/ann_module8.py | 10 ++++++++++ Lib/test/test_typing.py | 37 +++++++++++++++++++++++++++++++++++++ Lib/typing.py | 14 +++++++++++--- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 Lib/test/ann_module8.py diff --git a/Lib/test/ann_module8.py b/Lib/test/ann_module8.py new file mode 100644 index 00000000000000..bd031481378415 --- /dev/null +++ b/Lib/test/ann_module8.py @@ -0,0 +1,10 @@ +# Test `@no_type_check`, +# see https://bugs.python.org/issue46571 + +class NoTypeCheck_Outer: + class Inner: + x: int + + +def NoTypeCheck_function(arg: int) -> int: + ... diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index aa29c48654cefa..e9e07b90043c93 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2669,6 +2669,18 @@ def test_errors(self): cast('hello', 42) +# We need this to make sure that `@no_type_check` respects `__module__` attr: +from test import ann_module8 + +@no_type_check +class NoTypeCheck_Outer: + Inner = ann_module8.NoTypeCheck_Outer.Inner + +@no_type_check +class NoTypeCheck_WithFunction: + NoTypeCheck_function = ann_module8.NoTypeCheck_function + + class ForwardRefTests(BaseTestCase): def test_basics(self): @@ -3004,6 +3016,30 @@ class D: not_modified.__no_type_check__ self.assertNotEqual(get_type_hints(not_modified), {}) + def test_no_type_check_class_and_static_methods(self): + @no_type_check + class Some: + @staticmethod + def st(x: int) -> int: ... + @classmethod + def cl(cls, y: int) -> int: ... + + self.assertTrue(Some.st.__no_type_check__) + self.assertEqual(get_type_hints(Some.st), {}) + self.assertTrue(Some.cl.__no_type_check__) + self.assertEqual(get_type_hints(Some.cl), {}) + + def test_no_type_check_other_module(self): + self.assertTrue(NoTypeCheck_Outer.__no_type_check__) + with self.assertRaises(AttributeError): + ann_module8.NoTypeCheck_Outer.__no_type_check__ + with self.assertRaises(AttributeError): + ann_module8.NoTypeCheck_Outer.Inner.__no_type_check__ + + self.assertTrue(NoTypeCheck_WithFunction.__no_type_check__) + with self.assertRaises(AttributeError): + ann_module8.NoTypeCheck_function.__no_type_check__ + def test_no_type_check_foreign_functions(self): # We should not modify this function: def some(*args: int) -> int: @@ -3017,6 +3053,7 @@ class A: with self.assertRaises(AttributeError): some.__no_type_check__ + self.assertEqual(get_type_hints(some), {'args': int, 'return': int}) def test_no_type_check_lambda(self): @no_type_check diff --git a/Lib/typing.py b/Lib/typing.py index 30d57e4e277b48..c39d36cb2604cc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1959,15 +1959,23 @@ def no_type_check(arg): This mutates the function(s) or class(es) in place. """ if isinstance(arg, type): - for obj in arg.__dict__.values(): - if (hasattr(obj, '__qualname__') - and obj.__qualname__ != f'{arg.__qualname__}.{obj.__name__}'): + for key in dir(arg): + obj = getattr(arg, key) + if ( + not hasattr(obj, '__qualname__') + or obj.__qualname__ != f'{arg.__qualname__}.{obj.__name__}' + or getattr(obj, '__module__', None) != arg.__module__ + ): # We only modify objects that are defined in this type directly. # If classes / methods are nested in multiple layers, # we will modify them when processing their direct holders. continue + # Instance, class, and static methods: if isinstance(obj, types.FunctionType): obj.__no_type_check__ = True + if isinstance(obj, types.MethodType): + obj.__func__.__no_type_check__ = True + # Nested types: if isinstance(obj, type): no_type_check(obj) try: From 24d737eb138523c4b047beef72157bcd1c064ac0 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 2 Feb 2022 14:14:49 +0300 Subject: [PATCH 3/3] Update 2022-02-01-11-21-34.bpo-46571.L40xUJ.rst --- .../next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst b/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst index 7a6e0668e18acf..f56c9e4fd76d34 100644 --- a/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst +++ b/Misc/NEWS.d/next/Library/2022-02-01-11-21-34.bpo-46571.L40xUJ.rst @@ -1,2 +1,4 @@ -Improve :func:`typing.no_type_check` to not modify foreign functions and -types. +Improve :func:`typing.no_type_check`. + +Now it does not modify external classes and functions. +We also now correctly mark classmethods as not to be type checked.