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:
Łukasz Langa 2017-09-14 14:33:00 -04:00 committed by GitHub
parent d393c1b227
commit f350a268a7
4 changed files with 97 additions and 23 deletions

View File

@ -1,14 +1,53 @@
"""Module for testing the behavior of generics across different modules."""
from typing import TypeVar, Generic
T = TypeVar('T')
import sys
from textwrap import dedent
from typing import TypeVar, Generic, Optional
class A(Generic[T]):
pass
if sys.version_info[:2] >= (3, 6):
exec(dedent("""
default_a: Optional['A'] = None
default_b: Optional['B'] = None
T = TypeVar('T')
class B(Generic[T]):
class A(Generic[T]):
pass
some_b: 'B'
class B(Generic[T]):
class A(Generic[T]):
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__
)

View File

@ -3,7 +3,7 @@ import collections
import pickle
import re
import sys
from unittest import TestCase, main, skipUnless, SkipTest
from unittest import TestCase, main, skipUnless, SkipTest, expectedFailure
from copy import copy, deepcopy
from typing import Any, NoReturn
@ -30,6 +30,13 @@ except ImportError:
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):
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
@ -836,10 +843,6 @@ class GenericTests(BaseTestCase):
self.assertEqual(Callable[..., GenericMeta].__args__, (Ellipsis, GenericMeta))
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]):
...
@ -1619,6 +1622,10 @@ class XRepr(NamedTuple):
def __add__(self, other):
return 0
class HasForeignBaseClass(mod_generics_cache.A):
some_xrepr: 'XRepr'
other_a: 'mod_generics_cache.A'
async def g_with(am: AsyncContextManager[int]):
x: int
async with am as x:
@ -1658,9 +1665,19 @@ class GetTypeHintTests(BaseTestCase):
self.assertEqual(gth(ann_module2), {})
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')
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]})
self.assertIsInstance(gth(ann_module.j_class), dict)
self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type})
@ -1671,8 +1688,15 @@ class GetTypeHintTests(BaseTestCase):
{'y': Optional[ann_module.C]})
self.assertEqual(gth(ann_module.S), {'x': str, 'y': str})
self.assertEqual(gth(ann_module.foo), {'x': int})
self.assertEqual(gth(NoneAndForward, globals()),
self.assertEqual(gth(NoneAndForward),
{'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')
def test_respect_no_type_check(self):

View File

@ -1481,8 +1481,9 @@ def get_type_hints(obj, globalns=None, localns=None):
search order is locals first, then globals.
- 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
object does not appear to have globals, an exception is raised.
globals from obj (or the respective module's globals for classes),
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
locals.
@ -1493,25 +1494,33 @@ def get_type_hints(obj, globalns=None, localns=None):
if getattr(obj, '__no_type_check__', None):
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.
if isinstance(obj, type):
hints = {}
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__', {})
for name, value in ann.items():
if value is None:
value = type(None)
if isinstance(value, str):
value = _ForwardRef(value)
value = _eval_type(value, globalns, localns)
value = _eval_type(value, base_globals, localns)
hints[name] = value
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)
if hints is None:
# Return empty annotations for something that _could_ have them.

View File

@ -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).