mirror of
https://github.com/python/cpython.git
synced 2025-09-27 10:50:04 +00:00
bpo-28556: typing.get_type_hints: better globalns for classes and modules (#3582)
This makes the default behavior (without specifying `globalns` manually) more predictable for users, finds the right globalns automatically. Implementation for classes assumes has a `__module__` attribute and that module is present in `sys.modules`. It does this recursively for all bases in the MRO. For modules, the implementation just uses their `__dict__` directly. This is backwards compatible, will just raise fewer exceptions in naive user code. Originally implemented and reviewed at https://github.com/python/typing/pull/470.
This commit is contained in:
parent
d393c1b227
commit
f350a268a7
4 changed files with 97 additions and 23 deletions
|
@ -1,14 +1,53 @@
|
||||||
"""Module for testing the behavior of generics across different modules."""
|
"""Module for testing the behavior of generics across different modules."""
|
||||||
|
|
||||||
from typing import TypeVar, Generic
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import TypeVar, Generic, Optional
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[:2] >= (3, 6):
|
||||||
|
exec(dedent("""
|
||||||
|
default_a: Optional['A'] = None
|
||||||
|
default_b: Optional['B'] = None
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
class A(Generic[T]):
|
class A(Generic[T]):
|
||||||
pass
|
some_b: 'B'
|
||||||
|
|
||||||
|
|
||||||
class B(Generic[T]):
|
class B(Generic[T]):
|
||||||
class A(Generic[T]):
|
class A(Generic[T]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
my_inner_a1: 'B.A'
|
||||||
|
my_inner_a2: A
|
||||||
|
my_outer_a: 'A' # unless somebody calls get_type_hints with localns=B.__dict__
|
||||||
|
"""))
|
||||||
|
else: # This should stay in sync with the syntax above.
|
||||||
|
__annotations__ = dict(
|
||||||
|
default_a=Optional['A'],
|
||||||
|
default_b=Optional['B'],
|
||||||
|
)
|
||||||
|
default_a = None
|
||||||
|
default_b = None
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class A(Generic[T]):
|
||||||
|
__annotations__ = dict(
|
||||||
|
some_b='B'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class B(Generic[T]):
|
||||||
|
class A(Generic[T]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
__annotations__ = dict(
|
||||||
|
my_inner_a1='B.A',
|
||||||
|
my_inner_a2=A,
|
||||||
|
my_outer_a='A' # unless somebody calls get_type_hints with localns=B.__dict__
|
||||||
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import collections
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from unittest import TestCase, main, skipUnless, SkipTest
|
from unittest import TestCase, main, skipUnless, SkipTest, expectedFailure
|
||||||
from copy import copy, deepcopy
|
from copy import copy, deepcopy
|
||||||
|
|
||||||
from typing import Any, NoReturn
|
from typing import Any, NoReturn
|
||||||
|
@ -30,6 +30,13 @@ except ImportError:
|
||||||
import collections as collections_abc # Fallback for PY3.2.
|
import collections as collections_abc # Fallback for PY3.2.
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import mod_generics_cache
|
||||||
|
except ImportError:
|
||||||
|
# try to use the builtin one, Python 3.5+
|
||||||
|
from test import mod_generics_cache
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(TestCase):
|
class BaseTestCase(TestCase):
|
||||||
|
|
||||||
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
|
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
|
||||||
|
@ -836,10 +843,6 @@ class GenericTests(BaseTestCase):
|
||||||
self.assertEqual(Callable[..., GenericMeta].__args__, (Ellipsis, GenericMeta))
|
self.assertEqual(Callable[..., GenericMeta].__args__, (Ellipsis, GenericMeta))
|
||||||
|
|
||||||
def test_generic_hashes(self):
|
def test_generic_hashes(self):
|
||||||
try:
|
|
||||||
from test import mod_generics_cache
|
|
||||||
except ImportError: # for Python 3.4 and previous versions
|
|
||||||
import mod_generics_cache
|
|
||||||
class A(Generic[T]):
|
class A(Generic[T]):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -1619,6 +1622,10 @@ class XRepr(NamedTuple):
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
class HasForeignBaseClass(mod_generics_cache.A):
|
||||||
|
some_xrepr: 'XRepr'
|
||||||
|
other_a: 'mod_generics_cache.A'
|
||||||
|
|
||||||
async def g_with(am: AsyncContextManager[int]):
|
async def g_with(am: AsyncContextManager[int]):
|
||||||
x: int
|
x: int
|
||||||
async with am as x:
|
async with am as x:
|
||||||
|
@ -1658,9 +1665,19 @@ class GetTypeHintTests(BaseTestCase):
|
||||||
self.assertEqual(gth(ann_module2), {})
|
self.assertEqual(gth(ann_module2), {})
|
||||||
self.assertEqual(gth(ann_module3), {})
|
self.assertEqual(gth(ann_module3), {})
|
||||||
|
|
||||||
|
@skipUnless(PY36, 'Python 3.6 required')
|
||||||
|
@expectedFailure
|
||||||
|
def test_get_type_hints_modules_forwardref(self):
|
||||||
|
# FIXME: This currently exposes a bug in typing. Cached forward references
|
||||||
|
# don't account for the case where there are multiple types of the same
|
||||||
|
# name coming from different modules in the same program.
|
||||||
|
mgc_hints = {'default_a': Optional[mod_generics_cache.A],
|
||||||
|
'default_b': Optional[mod_generics_cache.B]}
|
||||||
|
self.assertEqual(gth(mod_generics_cache), mgc_hints)
|
||||||
|
|
||||||
@skipUnless(PY36, 'Python 3.6 required')
|
@skipUnless(PY36, 'Python 3.6 required')
|
||||||
def test_get_type_hints_classes(self):
|
def test_get_type_hints_classes(self):
|
||||||
self.assertEqual(gth(ann_module.C, ann_module.__dict__),
|
self.assertEqual(gth(ann_module.C), # gth will find the right globalns
|
||||||
{'y': Optional[ann_module.C]})
|
{'y': Optional[ann_module.C]})
|
||||||
self.assertIsInstance(gth(ann_module.j_class), dict)
|
self.assertIsInstance(gth(ann_module.j_class), dict)
|
||||||
self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type})
|
self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type})
|
||||||
|
@ -1671,8 +1688,15 @@ class GetTypeHintTests(BaseTestCase):
|
||||||
{'y': Optional[ann_module.C]})
|
{'y': Optional[ann_module.C]})
|
||||||
self.assertEqual(gth(ann_module.S), {'x': str, 'y': str})
|
self.assertEqual(gth(ann_module.S), {'x': str, 'y': str})
|
||||||
self.assertEqual(gth(ann_module.foo), {'x': int})
|
self.assertEqual(gth(ann_module.foo), {'x': int})
|
||||||
self.assertEqual(gth(NoneAndForward, globals()),
|
self.assertEqual(gth(NoneAndForward),
|
||||||
{'parent': NoneAndForward, 'meaning': type(None)})
|
{'parent': NoneAndForward, 'meaning': type(None)})
|
||||||
|
self.assertEqual(gth(HasForeignBaseClass),
|
||||||
|
{'some_xrepr': XRepr, 'other_a': mod_generics_cache.A,
|
||||||
|
'some_b': mod_generics_cache.B})
|
||||||
|
self.assertEqual(gth(mod_generics_cache.B),
|
||||||
|
{'my_inner_a1': mod_generics_cache.B.A,
|
||||||
|
'my_inner_a2': mod_generics_cache.B.A,
|
||||||
|
'my_outer_a': mod_generics_cache.A})
|
||||||
|
|
||||||
@skipUnless(PY36, 'Python 3.6 required')
|
@skipUnless(PY36, 'Python 3.6 required')
|
||||||
def test_respect_no_type_check(self):
|
def test_respect_no_type_check(self):
|
||||||
|
|
|
@ -1481,8 +1481,9 @@ def get_type_hints(obj, globalns=None, localns=None):
|
||||||
search order is locals first, then globals.
|
search order is locals first, then globals.
|
||||||
|
|
||||||
- If no dict arguments are passed, an attempt is made to use the
|
- If no dict arguments are passed, an attempt is made to use the
|
||||||
globals from obj, and these are also used as the locals. If the
|
globals from obj (or the respective module's globals for classes),
|
||||||
object does not appear to have globals, an exception is raised.
|
and these are also used as the locals. If the object does not appear
|
||||||
|
to have globals, an empty dictionary is used.
|
||||||
|
|
||||||
- If one dict argument is passed, it is used for both globals and
|
- If one dict argument is passed, it is used for both globals and
|
||||||
locals.
|
locals.
|
||||||
|
@ -1493,25 +1494,33 @@ def get_type_hints(obj, globalns=None, localns=None):
|
||||||
|
|
||||||
if getattr(obj, '__no_type_check__', None):
|
if getattr(obj, '__no_type_check__', None):
|
||||||
return {}
|
return {}
|
||||||
if globalns is None:
|
|
||||||
globalns = getattr(obj, '__globals__', {})
|
|
||||||
if localns is None:
|
|
||||||
localns = globalns
|
|
||||||
elif localns is None:
|
|
||||||
localns = globalns
|
|
||||||
# Classes require a special treatment.
|
# Classes require a special treatment.
|
||||||
if isinstance(obj, type):
|
if isinstance(obj, type):
|
||||||
hints = {}
|
hints = {}
|
||||||
for base in reversed(obj.__mro__):
|
for base in reversed(obj.__mro__):
|
||||||
|
if globalns is None:
|
||||||
|
base_globals = sys.modules[base.__module__].__dict__
|
||||||
|
else:
|
||||||
|
base_globals = globalns
|
||||||
ann = base.__dict__.get('__annotations__', {})
|
ann = base.__dict__.get('__annotations__', {})
|
||||||
for name, value in ann.items():
|
for name, value in ann.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
value = type(None)
|
value = type(None)
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = _ForwardRef(value)
|
value = _ForwardRef(value)
|
||||||
value = _eval_type(value, globalns, localns)
|
value = _eval_type(value, base_globals, localns)
|
||||||
hints[name] = value
|
hints[name] = value
|
||||||
return hints
|
return hints
|
||||||
|
|
||||||
|
if globalns is None:
|
||||||
|
if isinstance(obj, types.ModuleType):
|
||||||
|
globalns = obj.__dict__
|
||||||
|
else:
|
||||||
|
globalns = getattr(obj, '__globals__', {})
|
||||||
|
if localns is None:
|
||||||
|
localns = globalns
|
||||||
|
elif localns is None:
|
||||||
|
localns = globalns
|
||||||
hints = getattr(obj, '__annotations__', None)
|
hints = getattr(obj, '__annotations__', None)
|
||||||
if hints is None:
|
if hints is None:
|
||||||
# Return empty annotations for something that _could_ have them.
|
# Return empty annotations for something that _could_ have them.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
typing.get_type_hints now finds the right globalns for classes and modules
|
||||||
|
by default (when no ``globalns`` was specified by the caller).
|
Loading…
Add table
Add a link
Reference in a new issue