[Python-checkins] bpo-28556: typing.get_type_hints: better globalns for classes and modules (#3582)

Łukasz Langa webhook-mailer at python.org
Thu Sep 14 14:33:02 EDT 2017


https://github.com/python/cpython/commit/f350a268a7071ce7d7a5bb86a9b1229782d4963b
commit: f350a268a7071ce7d7a5bb86a9b1229782d4963b
branch: master
author: Łukasz Langa <lukasz at langa.pl>
committer: GitHub <noreply at github.com>
date: 2017年09月14日T14:33:00-04:00
summary:
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.
files:
A Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst
M Lib/test/mod_generics_cache.py
M Lib/test/test_typing.py
M Lib/typing.py
diff --git a/Lib/test/mod_generics_cache.py b/Lib/test/mod_generics_cache.py
index d9a60b4b28c..6d35c58396d 100644
--- a/Lib/test/mod_generics_cache.py
+++ b/Lib/test/mod_generics_cache.py
@@ -1,14 +1,53 @@
 """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]):
- pass
+ 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__
+ )
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index a351be1dc3e..87d707c1cde 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -3,7 +3,7 @@
 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 @@
 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 @@ def test_subscript_meta(self):
 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 @@ def __str__(self):
 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:
@@ -1659,8 +1666,18 @@ def test_get_type_hints_modules(self):
 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 @@ def test_get_type_hints_classes(self):
 {'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):
diff --git a/Lib/typing.py b/Lib/typing.py
index 609f813b01e..c00a3a10e1f 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -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.
diff --git a/Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst b/Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst
new file mode 100644
index 00000000000..8464d59a0cb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst
@@ -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).


More information about the Python-checkins mailing list

AltStyle によって変換されたページ (->オリジナル) /