Skip to content

Commit f350a26

Browse files
authored
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 python/typing#470.
1 parent d393c1b commit f350a26

File tree

4 files changed

+96
-22
lines changed

4 files changed

+96
-22
lines changed

Lib/test/mod_generics_cache.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,53 @@
11
"""Module for testing the behavior of generics across different modules."""
22

3-
from typing import TypeVar, Generic
3+
import sys
4+
from textwrap import dedent
5+
from typing import TypeVar, Generic, Optional
46

5-
T = TypeVar('T')
67

8+
if sys.version_info[:2] >= (3, 6):
9+
exec(dedent("""
10+
default_a: Optional['A'] = None
11+
default_b: Optional['B'] = None
712
8-
class A(Generic[T]):
9-
pass
13+
T = TypeVar('T')
1014
1115
12-
class B(Generic[T]):
1316
class A(Generic[T]):
14-
pass
17+
some_b: 'B'
18+
19+
20+
class B(Generic[T]):
21+
class A(Generic[T]):
22+
pass
23+
24+
my_inner_a1: 'B.A'
25+
my_inner_a2: A
26+
my_outer_a: 'A' # unless somebody calls get_type_hints with localns=B.__dict__
27+
"""))
28+
else: # This should stay in sync with the syntax above.
29+
__annotations__ = dict(
30+
default_a=Optional['A'],
31+
default_b=Optional['B'],
32+
)
33+
default_a = None
34+
default_b = None
35+
36+
T = TypeVar('T')
37+
38+
39+
class A(Generic[T]):
40+
__annotations__ = dict(
41+
some_b='B'
42+
)
43+
44+
45+
class B(Generic[T]):
46+
class A(Generic[T]):
47+
pass
48+
49+
__annotations__ = dict(
50+
my_inner_a1='B.A',
51+
my_inner_a2=A,
52+
my_outer_a='A' # unless somebody calls get_type_hints with localns=B.__dict__
53+
)

Lib/test/test_typing.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pickle
44
import re
55
import sys
6-
from unittest import TestCase, main, skipUnless, SkipTest
6+
from unittest import TestCase, main, skipUnless, SkipTest, expectedFailure
77
from copy import copy, deepcopy
88

99
from typing import Any, NoReturn
@@ -30,6 +30,13 @@
3030
import collections as collections_abc # Fallback for PY3.2.
3131

3232

33+
try:
34+
import mod_generics_cache
35+
except ImportError:
36+
# try to use the builtin one, Python 3.5+
37+
from test import mod_generics_cache
38+
39+
3340
class BaseTestCase(TestCase):
3441

3542
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
@@ -836,10 +843,6 @@ def test_subscript_meta(self):
836843
self.assertEqual(Callable[..., GenericMeta].__args__, (Ellipsis, GenericMeta))
837844

838845
def test_generic_hashes(self):
839-
try:
840-
from test import mod_generics_cache
841-
except ImportError: # for Python 3.4 and previous versions
842-
import mod_generics_cache
843846
class A(Generic[T]):
844847
...
845848

@@ -1619,6 +1622,10 @@ def __str__(self):
16191622
def __add__(self, other):
16201623
return 0
16211624
1625+
class HasForeignBaseClass(mod_generics_cache.A):
1626+
some_xrepr: 'XRepr'
1627+
other_a: 'mod_generics_cache.A'
1628+
16221629
async def g_with(am: AsyncContextManager[int]):
16231630
x: int
16241631
async with am as x:
@@ -1658,9 +1665,19 @@ def test_get_type_hints_modules(self):
16581665
self.assertEqual(gth(ann_module2), {})
16591666
self.assertEqual(gth(ann_module3), {})
16601667

1668+
@skipUnless(PY36, 'Python 3.6 required')
1669+
@expectedFailure
1670+
def test_get_type_hints_modules_forwardref(self):
1671+
# FIXME: This currently exposes a bug in typing. Cached forward references
1672+
# don't account for the case where there are multiple types of the same
1673+
# name coming from different modules in the same program.
1674+
mgc_hints = {'default_a': Optional[mod_generics_cache.A],
1675+
'default_b': Optional[mod_generics_cache.B]}
1676+
self.assertEqual(gth(mod_generics_cache), mgc_hints)
1677+
16611678
@skipUnless(PY36, 'Python 3.6 required')
16621679
def test_get_type_hints_classes(self):
1663-
self.assertEqual(gth(ann_module.C, ann_module.__dict__),
1680+
self.assertEqual(gth(ann_module.C), # gth will find the right globalns
16641681
{'y': Optional[ann_module.C]})
16651682
self.assertIsInstance(gth(ann_module.j_class), dict)
16661683
self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type})
@@ -1671,8 +1688,15 @@ def test_get_type_hints_classes(self):
16711688
{'y': Optional[ann_module.C]})
16721689
self.assertEqual(gth(ann_module.S), {'x': str, 'y': str})
16731690
self.assertEqual(gth(ann_module.foo), {'x': int})
1674-
self.assertEqual(gth(NoneAndForward, globals()),
1691+
self.assertEqual(gth(NoneAndForward),
16751692
{'parent': NoneAndForward, 'meaning': type(None)})
1693+
self.assertEqual(gth(HasForeignBaseClass),
1694+
{'some_xrepr': XRepr, 'other_a': mod_generics_cache.A,
1695+
'some_b': mod_generics_cache.B})
1696+
self.assertEqual(gth(mod_generics_cache.B),
1697+
{'my_inner_a1': mod_generics_cache.B.A,
1698+
'my_inner_a2': mod_generics_cache.B.A,
1699+
'my_outer_a': mod_generics_cache.A})
16761700

16771701
@skipUnless(PY36, 'Python 3.6 required')
16781702
def test_respect_no_type_check(self):

Lib/typing.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,8 +1481,9 @@ def get_type_hints(obj, globalns=None, localns=None):
14811481
search order is locals first, then globals.
14821482
14831483
- If no dict arguments are passed, an attempt is made to use the
1484-
globals from obj, and these are also used as the locals. If the
1485-
object does not appear to have globals, an exception is raised.
1484+
globals from obj (or the respective module's globals for classes),
1485+
and these are also used as the locals. If the object does not appear
1486+
to have globals, an empty dictionary is used.
14861487
14871488
- If one dict argument is passed, it is used for both globals and
14881489
locals.
@@ -1493,25 +1494,33 @@ def get_type_hints(obj, globalns=None, localns=None):
14931494

14941495
if getattr(obj, '__no_type_check__', None):
14951496
return {}
1496-
if globalns is None:
1497-
globalns = getattr(obj, '__globals__', {})
1498-
if localns is None:
1499-
localns = globalns
1500-
elif localns is None:
1501-
localns = globalns
15021497
# Classes require a special treatment.
15031498
if isinstance(obj, type):
15041499
hints = {}
15051500
for base in reversed(obj.__mro__):
1501+
if globalns is None:
1502+
base_globals = sys.modules[base.__module__].__dict__
1503+
else:
1504+
base_globals = globalns
15061505
ann = base.__dict__.get('__annotations__', {})
15071506
for name, value in ann.items():
15081507
if value is None:
15091508
value = type(None)
15101509
if isinstance(value, str):
15111510
value = _ForwardRef(value)
1512-
value = _eval_type(value, globalns, localns)
1511+
value = _eval_type(value, base_globals, localns)
15131512
hints[name] = value
15141513
return hints
1514+
1515+
if globalns is None:
1516+
if isinstance(obj, types.ModuleType):
1517+
globalns = obj.__dict__
1518+
else:
1519+
globalns = getattr(obj, '__globals__', {})
1520+
if localns is None:
1521+
localns = globalns
1522+
elif localns is None:
1523+
localns = globalns
15151524
hints = getattr(obj, '__annotations__', None)
15161525
if hints is None:
15171526
# Return empty annotations for something that _could_ have them.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
typing.get_type_hints now finds the right globalns for classes and modules
2+
by default (when no ``globalns`` was specified by the caller).

0 commit comments

Comments
 (0)