summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorNikita Sobolev <mail@sobolevn.me>2022-02-19 01:53:29 (GMT)
committerGitHub <noreply@github.com>2022-02-19 01:53:29 (GMT)
commit395029b0bd343648b4da8044c7509672ea768775 (patch)
treecdbf8b86c5bead4d8315c100a7f732f92902b817 /Lib
parentf80a97b492f41afd3c42bb2bd6da7b2828dca215 (diff)
downloadcpython-395029b0bd343648b4da8044c7509672ea768775.zip
cpython-395029b0bd343648b4da8044c7509672ea768775.tar.gz
cpython-395029b0bd343648b4da8044c7509672ea768775.tar.bz2
bpo-46571: improve `typing.no_type_check` to skip foreign objects (GH-31042)
There are several changes: 1. We now don't explicitly check for any base / sub types, because new name check covers it 2. I've also checked that `no_type_check` do not modify foreign functions. It was the same as with `type`s 3. I've also covered `except TypeError` in `no_type_check` with a simple test case, it was not covered at all 4. I also felt like adding `lambda` test is a good idea: because `lambda` is a bit of both in class bodies: a function and an assignment <!-- issue-number: [bpo-46571](https://bugs.python.org/issue46571) --> https://bugs.python.org/issue46571 <!-- /issue-number -->
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/ann_module8.py10
-rw-r--r--Lib/test/test_typing.py101
-rw-r--r--Lib/typing.py20
3 files changed, 126 insertions, 5 deletions
diff --git a/Lib/test/ann_module8.py b/Lib/test/ann_module8.py
new file mode 100644
index 0000000..bd03148
--- /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 1b3eb06..d24a357 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -2744,6 +2744,18 @@ class CastTests(BaseTestCase):
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):
@@ -3058,9 +3070,98 @@ class ForwardRefTests(BaseTestCase):
@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_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:
+ ...
+
+ @no_type_check
+ class A:
+ some_alias = some
+ some_class = classmethod(some)
+ some_static = staticmethod(some)
+
+ 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
+ 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 8f923fa..ad1435e 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2131,13 +2131,23 @@ 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 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: