Commit f350a268 authored by Łukasz Langa's avatar Łukasz Langa Committed by GitHub

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.
parent d393c1b2
"""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
T = TypeVar('T')
if sys.version_info[:2] >= (3, 6):
exec(dedent("""
default_a: Optional['A'] = None
default_b: Optional['B'] = None
class A(Generic[T]): T = TypeVar('T')
class A(Generic[T]):
some_b: 'B'
class B(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
class B(Generic[T]): T = TypeVar('T')
class A(Generic[T]):
__annotations__ = dict(
some_b='B'
)
class B(Generic[T]):
class A(Generic[T]): class A(Generic[T]):
pass 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 ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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.
......
typing.get_type_hints now finds the right globalns for classes and modules
by default (when no ``globalns`` was specified by the caller).
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment