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

Skip to content

Commit a03009c

Browse files
committed
bpo-46571: improve typing.no_type_check to skip foreign objects
1 parent ee0ac32 commit a03009c

File tree

4 files changed

+75
-7
lines changed

4 files changed

+75
-7
lines changed

Doc/library/typing.rst

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

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

20052005
This mutates the function(s) in place.
20062006

Lib/test/test_typing.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2965,9 +2965,73 @@ def meth(self, x: int): ...
29652965
@no_type_check
29662966
class D(C):
29672967
c = C
2968+
29682969
# verify that @no_type_check never affects bases
29692970
self.assertEqual(get_type_hints(C.meth), {'x': int})
29702971

2972+
# and never child classes:
2973+
class Child(D):
2974+
def foo(self, x: int): ...
2975+
2976+
self.assertEqual(get_type_hints(Child.foo), {'x': int})
2977+
2978+
def test_no_type_check_nested_types(self):
2979+
# See https://bugs.python.org/issue46571
2980+
class Other:
2981+
o: int
2982+
class B: # Has the same `__name__`` as `A.B` and different `__qualname__`
2983+
o: int
2984+
@no_type_check
2985+
class A:
2986+
a: int
2987+
class B:
2988+
b: int
2989+
class C:
2990+
c: int
2991+
class D:
2992+
d: int
2993+
2994+
Other = Other
2995+
2996+
for klass in [A, A.B, A.B.C, A.D]:
2997+
with self.subTest(klass=klass):
2998+
self.assertTrue(klass.__no_type_check__)
2999+
self.assertEqual(get_type_hints(klass), {})
3000+
3001+
for not_modified in [Other, B]:
3002+
with self.subTest(not_modified=not_modified):
3003+
with self.assertRaises(AttributeError):
3004+
not_modified.__no_type_check__
3005+
self.assertNotEqual(get_type_hints(not_modified), {})
3006+
3007+
def test_no_type_check_foreign_functions(self):
3008+
# We should not modify this function:
3009+
def some(*args: int) -> int:
3010+
...
3011+
3012+
@no_type_check
3013+
class A:
3014+
some_alias = some
3015+
some_class = classmethod(some)
3016+
some_static = staticmethod(some)
3017+
3018+
with self.assertRaises(AttributeError):
3019+
some.__no_type_check__
3020+
3021+
def test_no_type_check_lambda(self):
3022+
@no_type_check
3023+
class A:
3024+
# Corner case: `lambda` is both an assignment and a function:
3025+
bar: Callable[[int], int] = lambda arg: arg
3026+
3027+
self.assertTrue(A.bar.__no_type_check__)
3028+
self.assertEqual(get_type_hints(A.bar), {})
3029+
3030+
def test_no_type_check_TypeError(self):
3031+
# This simply should not fail with
3032+
# `TypeError: can't set attributes of built-in/extension type 'dict'`
3033+
no_type_check(dict)
3034+
29713035
def test_no_type_check_forward_ref_as_string(self):
29723036
class C:
29733037
foo: typing.ClassVar[int] = 7

Lib/typing.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1959,11 +1959,13 @@ def no_type_check(arg):
19591959
This mutates the function(s) or class(es) in place.
19601960
"""
19611961
if isinstance(arg, type):
1962-
arg_attrs = arg.__dict__.copy()
1963-
for attr, val in arg.__dict__.items():
1964-
if val in arg.__bases__ + (arg,):
1965-
arg_attrs.pop(attr)
1966-
for obj in arg_attrs.values():
1962+
for obj in arg.__dict__.values():
1963+
if (hasattr(obj, '__qualname__')
1964+
and obj.__qualname__ != f'{arg.__qualname__}.{obj.__name__}'):
1965+
# We only modify objects that are defined in this type directly.
1966+
# If classes / methods are nested in multiple layers,
1967+
# we will modify them when processing their direct holders.
1968+
continue
19671969
if isinstance(obj, types.FunctionType):
19681970
obj.__no_type_check__ = True
19691971
if isinstance(obj, type):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve :func:`typing.no_type_check` to not modify foreign functions and
2+
types.

0 commit comments

Comments
 (0)
0