]> granicus.if.org Git - python/commitdiff
bpo-28556: typing.get_type_hints: better globalns for classes and modules (#3582)
authorŁukasz Langa <lukasz@langa.pl>
Thu, 14 Sep 2017 18:33:00 +0000 (14:33 -0400)
committerGitHub <noreply@github.com>
Thu, 14 Sep 2017 18:33:00 +0000 (14:33 -0400)
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
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2017-09-14-11-02-56.bpo-28556.EUOiYs.rst [new file with mode: 0644]

index d9a60b4b28c325aea1b82fe888504c7491095b67..6d35c58396d424ccb6151908b8510001a77d4b8a 100644 (file)
@@ -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__
+        )
index a351be1dc3e59bf080366ad4d8087d6dd16687f8..87d707c1cde6f38479d033dab1c0ddb08a3c5a83 100644 (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):
index 609f813b01eb214a6cf888af3f33e25ec2ef8e40..c00a3a10e1fae1b1538fbb7f15ce7a0968d5adde 100644 (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.
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 (file)
index 0000000..8464d59
--- /dev/null
@@ -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).