mirror of
https://github.com/python/cpython.git
synced 2024-11-24 02:15:30 +08:00
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:
parent
f80a97b492
commit
395029b0bd
@ -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
10
Lib/test/ann_module8.py
Normal 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:
|
||||
...
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user