Skip to content

bpo-46571: improve typing.no_type_check to skip foreign objects #31042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 10 additions & 0 deletions Lib/test/ann_module8.py
Original file line number Diff line number Diff line change
@@ -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:
...
101 changes: 101 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -2965,9 +2977,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
Expand Down
20 changes: 15 additions & 5 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1959,13 +1959,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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.