8000 bpo-46571: improve `typing.no_type_check` to skip foreign objects (GH… · python/cpython@395029b · GitHub
[go: up one dir, main page]

Skip to content

Commit 395029b

Browse files
authored
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 -->
1 parent f80a97b commit 395029b

File tree

5 files changed

+132
-7
lines changed

5 files changed

+132
-7
lines changed

Doc/library/typing.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,8 +2145,8 @@ Functions and decorators
21452145
Decorator to indicate that annotations are not type hints.
21462146

21472147
This works as class or function :term:`decorator`. With a class, it
2148-
applies recursively to all methods defined in that class (but not
2149-
to methods defined in its superclasses or subclasses).
2148+
applies recursively to all methods and classes defined in that class
2149+
(but not to methods defined in its superclasses or subclasses).
21502150

21512151
This mutates the function(s) in place.
21522152

Lib/test/ann_module8.py

Lines changed: 10 additions & 0 deletions
< 8000 /tr>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Test `@no_type_check`,
2+
# see https://bugs.python.org/issue46571
3+
4+
class NoTypeCheck_Outer:
5+
class Inner:
6+
x: int
7+
8+
9+
def NoTypeCheck_function(arg: int) -> int:
10+
...

Lib/test/test_typing.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2744,6 +2744,18 @@ def test_errors(self):
27442744
cast('hello', 42)
27452745

27462746

2747+
# We need this to make sure that `@no_type_check` respects `__module__` attr:
2748+
from test import ann_module8
2749+
2750+
@no_type_check
2751+
class NoTypeCheck_Outer:
2752+
Inner = ann_module8.NoTypeCheck_Outer.Inner
2753+
2754+
@no_type_check
2755+
class NoTypeCheck_WithFunction:
2756+
NoTypeCheck_function = ann_module8.NoTypeCheck_function
2757+
2758+
27472759
class ForwardRefTests(BaseTestCase):
27482760

27492761
def test_basics(self):
@@ -3058,9 +3070,98 @@ def meth(self, x: int): ...
30583070
@no_type_check
30593071
class D(C):
30603072
c = C
3073+
30613074
# verify that @no_type_check never affects bases
30623075
self.assertEqual(get_type_hints(C.meth), {'x': int})
30633076

3077+
# and never child classes:
3078+
class Child(D):
3079+
def foo(self, x: int): ...
3080+
3081+
self.assertEqual(get_type_hints(Child.foo), {'x': int})
3082+
3083+
def test_no_type_check_nested_types(self):
3084+
# See https://bugs.python.org/issue46571
3085+
class Other:
3086+
o: int
3087+
class B: # Has the same `__name__`` as `A.B` and different `__qualname__`
3088+
o: int
3089+
@no_type_check
3090+
class A:
3091+
a: int
3092+
class B:
3093+
b: int
3094+
class C:
3095+
c: int
3096+
class D:
3097+
d: int
3098+
3099+
Other = Other
3100+
3101+
for klass in [A, A.B, A.B.C, A.D]:
3102+
with self.subTest(klass=klass):
3103+
self.assertTrue(klass.__no_type_check__)
3104+
self.assertEqual(get_type_hints(klass), {})
3105+
3106+
for not_modified in [Other, B]:
3107+
with self.subTest(not_modified=not_modified):
3108+
with self.assertRaises(AttributeError):
3109+
not_modified.__no_type_check__
3110+
self.assertNotEqual(get_type_hints(not_modified), {})
3111+
3112+
def test_no_type_check_class_and_static_methods(self):
3113+
@no_type_check
3114+
class Some:
3115+
@staticmethod
3116+
def st(x: int) -> int: ...
3117+
@classmethod
3118+
def cl(cls, y: int) -> int: ...
3119+
3120+
self.assertTrue(Some.st.__no_type_check__)
3121+
self.assertEqual(get_type_hints(Some.st), {})
3122+
self.assertTrue(Some.cl.__no_type_check__)
3123+
self.assertEqual(get_type_hints(Some.cl), {})
3124+
3125+
def test_no_type_check_other_module(self):
3126+
self.assertTrue(NoTypeCheck_Outer.__no_type_check__)
3127+
with self.assertRaises(AttributeError):
3128+
ann_module8.NoTypeCheck_Outer.__no_type_check__
3129+
with self.assertRaises(AttributeError):
3130+
ann_module8.NoTypeCheck_Outer.Inner.__no_type_check__
3131+
3132+
self.assertTrue(NoTypeCheck_WithFunction.__no_type_check__)
3133+
with self.assertRaises(AttributeError):
3134+
ann_module8.NoTypeCheck_function.__no_type_check__
3135+
3136+
def test_no_type_check_foreign_functions(self):
3137+
# We should not modify this function:
3138+
def some(*args: int) -> int:
3139+
...
3140+
3141+
@no_type_check
3142+
class A:
3143+
some_alias = some
3144+
some_class = classmethod(some)
3145+
some_static = staticmethod(some)
3146+
3147+
with self.assertRaises(AttributeError):
3148+
some.__no_type_check__
3149+
self.assertEqual(get_type_hints(some), {'args': int, 'return': int})
3150+
3151+
def test_no_type_check_lambda(self):
3152+
@no_type_check
3153+
class A:
3154+
# Corner case: `lambda` is both an assignment and a function:
3155+
bar: Callable[[int], int] = lambda arg: arg
3156+
3157+
self.assertTrue(A.bar.__no_type_check__)
3158+
self.assertEqual(get_type_hints(A.bar), {})
3159+
3160+
def test_no_type_check_TypeError(self):
3161+
# This simply should not fail with
3162+
# `TypeError: can't set attributes of built-in/extension type 'dict'`
3163+
no_type_check(dict)
3164+
30643165
def test_no_type_check_forward_ref_as_string(self):
30653166
class C:
30663167
foo: typing.ClassVar[int] = 7

Lib/typing.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,13 +2131,23 @@ def no_type_check(arg):
21312131
This mutates the function(s) or class(es) in place.
21322132
"""
21332133
if isinstance(arg, type):
2134-
arg_attrs = arg.__dict__.copy()
2135-
for attr, val in arg.__dict__.items():
2136-
if val in arg.__bases__ + (arg,):
2137-
arg_attrs.pop(attr)
2138-
for obj in arg_attrs.values():
2134+
for key in dir(arg):
2135+
obj = getattr(arg, key)
2136+
if (
2137+
not hasattr(obj, '__qualname__')
2138+
or obj.__qualname__ != f'{arg.__qualname__}.{obj.__name__}'
2139+
or getattr(obj, '__module__', None) != arg.__module__
2140+
):
2141+
# We only modify objects that are defined in this type directly.
2142+
# If classes / methods are nested in multiple layers,
2143+
# we will modify them when processing their direct holders.
2144+
continue
2145+
# Instance, class, and static methods:
21392146
if isinstance(obj, types.FunctionType):
21402147
obj.__no_type_check__ = True
2148+
if isinstance(obj, types.MethodType):
2149+
obj.__func__.__no_type_check__ = True
2150+
# Nested types:
21412151
if isinstance(obj, type):
21422152
no_type_check(obj)
21432153
try:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve :func:`typing.no_type_check`.
2+
3+
Now it does not modify external classes and functions.
4+
We also now correctly mark classmethods as not to be type checked.

0 commit comments

Comments
 (0)
0