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 -->
This commit is contained in:
Nikita Sobolev 2022-02-19 04:53:29 +03:00 committed by GitHub
parent f80a97b492
commit 395029b0bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 7 deletions

View file

@ -2145,8 +2145,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.

10
Lib/test/ann_module8.py Normal file
View file

@ -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:
...

View file

@ -2744,6 +2744,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):
@ -3058,9 +3070,98 @@ 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_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

View file

@ -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:

View file

@ -0,0 +1,4 @@
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.