From f350a268a7071ce7d7a5bb86a9b1229782d4963b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 14 Sep 2017 14:33:00 -0400 Subject: [PATCH] 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. --- Lib/test/mod_generics_cache.py | 53 ++++++++++++++++--- Lib/test/test_typing.py | 38 ++++++++++--- Lib/typing.py | 27 ++++++---- .../2017-09-14-11-02-56.bpo-28556.EUOiYs.rst | 2 + 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst 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 - -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__ + ) 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 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): 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).