[Python-checkins] cpython: Issue #28079: Update typing and test typing from python/typing repo.

guido.van.rossum python-checkins at python.org
Sun Sep 11 18:31:41 EDT 2016


https://hg.python.org/cpython/rev/b66e0856ab24
changeset:   103662:b66e0856ab24
user:        Guido van Rossum <guido at dropbox.com>
date:        Sun Sep 11 15:31:27 2016 -0700
summary:
  Issue #28079: Update typing and test typing from python/typing repo.

Ivan Levkivskyi (3.6 version)

files:
  Lib/test/test_typing.py |  143 +++++++++----
  Lib/typing.py           |  294 ++++++++++++++++++---------
  2 files changed, 291 insertions(+), 146 deletions(-)


diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -4,8 +4,6 @@
 import re
 import sys
 from unittest import TestCase, main, skipUnless, SkipTest
-from collections import ChainMap
-from test import ann_module, ann_module2, ann_module3
 
 from typing import Any
 from typing import TypeVar, AnyStr
@@ -969,46 +967,6 @@
         right_hints = get_type_hints(t.add_right, globals(), locals())
         self.assertEqual(right_hints['node'], Optional[Node[T]])
 
-    def test_get_type_hints(self):
-        gth = get_type_hints
-        self.assertEqual(gth(ann_module), {'x': int, 'y': str})
-        self.assertEqual(gth(ann_module.C, ann_module.__dict__),
-                         ChainMap({'y': Optional[ann_module.C]}, {}))
-        self.assertEqual(gth(ann_module2), {})
-        self.assertEqual(gth(ann_module3), {})
-        self.assertEqual(repr(gth(ann_module.j_class)), 'ChainMap({}, {})')
-        self.assertEqual(gth(ann_module.M), ChainMap({'123': 123, 'o': type},
-                                                     {}, {}))
-        self.assertEqual(gth(ann_module.D),
-                         ChainMap({'j': str, 'k': str,
-                                   'y': Optional[ann_module.C]}, {}))
-        self.assertEqual(gth(ann_module.Y), ChainMap({'z': int}, {}))
-        self.assertEqual(gth(ann_module.h_class),
-                         ChainMap({}, {'y': Optional[ann_module.C]}, {}))
-        self.assertEqual(gth(ann_module.S), ChainMap({'x': str, 'y': str},
-                                                    {}))
-        self.assertEqual(gth(ann_module.foo), {'x': int})
-
-        def testf(x, y): ...
-        testf.__annotations__['x'] = 'int'
-        self.assertEqual(gth(testf), {'x': int})
-        self.assertEqual(gth(ann_module2.NTC.meth), {})
-
-        # interactions with ClassVar
-        class B:
-            x: ClassVar[Optional['B']] = None
-            y: int
-        class C(B):
-            z: ClassVar['C'] = B()
-        class G(Generic[T]):
-            lst: ClassVar[List[T]] = []
-        self.assertEqual(gth(B, locals()),
-                         ChainMap({'y': int, 'x': ClassVar[Optional[B]]}, {}))
-        self.assertEqual(gth(C, locals()),
-                        ChainMap({'z': ClassVar[C]},
-                                 {'y': int, 'x': ClassVar[Optional[B]]}, {}))
-        self.assertEqual(gth(G), ChainMap({'lst': ClassVar[List[T]]},{},{}))
-
     def test_forwardref_instance_type_error(self):
         fr = typing._ForwardRef('int')
         with self.assertRaises(TypeError):
@@ -1198,6 +1156,84 @@
 if PY35:
     exec(PY35_TESTS)
 
+PY36 = sys.version_info[:2] >= (3, 6)
+
+PY36_TESTS = """
+from test import ann_module, ann_module2, ann_module3
+from collections import ChainMap
+
+class B:
+    x: ClassVar[Optional['B']] = None
+    y: int
+class CSub(B):
+    z: ClassVar['CSub'] = B()
+class G(Generic[T]):
+    lst: ClassVar[List[T]] = []
+
+class CoolEmployee(NamedTuple):
+    name: str
+    cool: int
+"""
+
+if PY36:
+    exec(PY36_TESTS)
+
+gth = get_type_hints
+
+class GetTypeHintTests(BaseTestCase):
+    @skipUnless(PY36, 'Python 3.6 required')
+    def test_get_type_hints_modules(self):
+        self.assertEqual(gth(ann_module), {'x': int, 'y': str})
+        self.assertEqual(gth(ann_module2), {})
+        self.assertEqual(gth(ann_module3), {})
+
+    @skipUnless(PY36, 'Python 3.6 required')
+    def test_get_type_hints_classes(self):
+        self.assertEqual(gth(ann_module.C, ann_module.__dict__),
+                         ChainMap({'y': Optional[ann_module.C]}, {}))
+        self.assertEqual(repr(gth(ann_module.j_class)), 'ChainMap({}, {})')
+        self.assertEqual(gth(ann_module.M), ChainMap({'123': 123, 'o': type},
+                                                     {}, {}))
+        self.assertEqual(gth(ann_module.D),
+                         ChainMap({'j': str, 'k': str,
+                                   'y': Optional[ann_module.C]}, {}))
+        self.assertEqual(gth(ann_module.Y), ChainMap({'z': int}, {}))
+        self.assertEqual(gth(ann_module.h_class),
+                         ChainMap({}, {'y': Optional[ann_module.C]}, {}))
+        self.assertEqual(gth(ann_module.S), ChainMap({'x': str, 'y': str},
+                                                     {}))
+        self.assertEqual(gth(ann_module.foo), {'x': int})
+
+    @skipUnless(PY36, 'Python 3.6 required')
+    def test_respect_no_type_check(self):
+        @no_type_check
+        class NoTpCheck:
+            class Inn:
+                def __init__(self, x: 'not a type'): ...
+        self.assertTrue(NoTpCheck.__no_type_check__)
+        self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__)
+        self.assertEqual(gth(ann_module2.NTC.meth), {})
+        class ABase(Generic[T]):
+            def meth(x: int): ...
+        @no_type_check
+        class Der(ABase): ...
+        self.assertEqual(gth(ABase.meth), {'x': int})
+
+
+    def test_previous_behavior(self):
+        def testf(x, y): ...
+        testf.__annotations__['x'] = 'int'
+        self.assertEqual(gth(testf), {'x': int})
+
+    @skipUnless(PY36, 'Python 3.6 required')
+    def test_get_type_hints_ClassVar(self):
+        self.assertEqual(gth(B, globals()),
+                         ChainMap({'y': int, 'x': ClassVar[Optional[B]]}, {}))
+        self.assertEqual(gth(CSub, globals()),
+                         ChainMap({'z': ClassVar[CSub]},
+                                  {'y': int, 'x': ClassVar[Optional[B]]}, {}))
+        self.assertEqual(gth(G), ChainMap({'lst': ClassVar[List[T]]},{},{}))
+
 
 class CollectionsAbcTests(BaseTestCase):
 
@@ -1505,6 +1541,18 @@
 
         joe = new_user(BasicUser)
 
+    def test_type_optional(self):
+        A = Optional[Type[BaseException]]
+
+        def foo(a: A) -> Optional[BaseException]:
+            if a is None:
+                return None
+            else:
+                return a()
+
+        assert isinstance(foo(KeyboardInterrupt), KeyboardInterrupt)
+        assert foo(None) is None
+
 
 class NewTypeTests(BaseTestCase):
 
@@ -1542,6 +1590,17 @@
         self.assertEqual(Emp._fields, ('name', 'id'))
         self.assertEqual(Emp._field_types, dict(name=str, id=int))
 
+    @skipUnless(PY36, 'Python 3.6 required')
+    def test_annotation_usage(self):
+        tim = CoolEmployee('Tim', 9000)
+        self.assertIsInstance(tim, CoolEmployee)
+        self.assertIsInstance(tim, tuple)
+        self.assertEqual(tim.name, 'Tim')
+        self.assertEqual(tim.cool, 9000)
+        self.assertEqual(CoolEmployee.__name__, 'CoolEmployee')
+        self.assertEqual(CoolEmployee._fields, ('name', 'cool'))
+        self.assertEqual(CoolEmployee._field_types, dict(name=str, cool=int))
+
     def test_pickle(self):
         global Emp  # pickle wants to reference the class by name
         Emp = NamedTuple('Emp', [('name', str), ('id', int)])
diff --git a/Lib/typing.py b/Lib/typing.py
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -6,11 +6,12 @@
 import re as stdlib_re  # Avoid confusion with the re we export.
 import sys
 import types
-
 try:
     import collections.abc as collections_abc
 except ImportError:
     import collections as collections_abc  # Fallback for PY3.2.
+if sys.version_info[:2] >= (3, 3):
+    from collections import ChainMap
 
 
 # Please keep __all__ alphabetized within each category.
@@ -1204,53 +1205,135 @@
     return res
 
 
-def get_type_hints(obj, globalns=None, localns=None):
-    """Return type hints for an object.
+if sys.version_info[:2] >= (3, 3):
+    def get_type_hints(obj, globalns=None, localns=None):
+        """Return type hints for an object.
 
-    This is often the same as obj.__annotations__, but it handles
-    forward references encoded as string literals, and if necessary
-    adds Optional[t] if a default value equal to None is set.
+        This is often the same as obj.__annotations__, but it handles
+        forward references encoded as string literals, and if necessary
+        adds Optional[t] if a default value equal to None is set.
 
-    The argument may be a module, class, method, or function. The annotations
-    are returned as a dictionary, or in the case of a class, a ChainMap of
-    dictionaries.
+        The argument may be a module, class, method, or function. The annotations
+        are returned as a dictionary, or in the case of a class, a ChainMap of
+        dictionaries.
 
-    TypeError is raised if the argument is not of a type that can contain
-    annotations, and an empty dictionary is returned if no annotations are
-    present.
+        TypeError is raised if the argument is not of a type that can contain
+        annotations, and an empty dictionary is returned if no annotations are
+        present.
 
-    BEWARE -- the behavior of globalns and localns is counterintuitive
-    (unless you are familiar with how eval() and exec() work).  The
-    search order is locals first, then globals.
+        BEWARE -- the behavior of globalns and localns is counterintuitive
+        (unless you are familiar with how eval() and exec() work).  The
+        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.
+        - 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.
 
-    - If one dict argument is passed, it is used for both globals and
-      locals.
+        - If one dict argument is passed, it is used for both globals and
+          locals.
 
-    - If two dict arguments are passed, they specify globals and
-      locals, respectively.
-    """
+        - If two dict arguments are passed, they specify globals and
+          locals, respectively.
+        """
 
-    if getattr(obj, '__no_type_check__', None):
-        return {}
-    if globalns is None:
-        globalns = getattr(obj, '__globals__', {})
-        if localns is 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
-    elif localns is None:
-        localns = globalns
 
-    if (isinstance(obj, types.FunctionType) or
-        isinstance(obj, types.BuiltinFunctionType) or
-        isinstance(obj, types.MethodType)):
+        if (isinstance(obj, types.FunctionType) or
+            isinstance(obj, types.BuiltinFunctionType) or
+            isinstance(obj, types.MethodType)):
+            defaults = _get_defaults(obj)
+            hints = obj.__annotations__
+            for name, value in hints.items():
+                if value is None:
+                    value = type(None)
+                if isinstance(value, str):
+                    value = _ForwardRef(value)
+                value = _eval_type(value, globalns, localns)
+                if name in defaults and defaults[name] is None:
+                    value = Optional[value]
+                hints[name] = value
+            return hints
+
+        if isinstance(obj, types.ModuleType):
+            try:
+                hints = obj.__annotations__
+            except AttributeError:
+                return {}
+            # we keep only those annotations that can be accessed on module
+            members = obj.__dict__
+            hints = {name: value for name, value in hints.items()
+                                              if name in members}
+            for name, value in hints.items():
+                if value is None:
+                    value = type(None)
+                if isinstance(value, str):
+                    value = _ForwardRef(value)
+                value = _eval_type(value, globalns, localns)
+                hints[name] = value
+            return hints
+
+        if isinstance(object, type):
+            cmap = None
+            for base in reversed(obj.__mro__):
+                new_map = collections.ChainMap if cmap is None else cmap.new_child
+                try:
+                    hints = base.__dict__['__annotations__']
+                except KeyError:
+                    cmap = new_map()
+                else:
+                    for name, value in hints.items():
+                        if value is None:
+                            value = type(None)
+                        if isinstance(value, str):
+                            value = _ForwardRef(value)
+                        value = _eval_type(value, globalns, localns)
+                        hints[name] = value
+                    cmap = new_map(hints)
+            return cmap
+
+        raise TypeError('{!r} is not a module, class, method, '
+                        'or function.'.format(obj))
+
+else:
+    def get_type_hints(obj, globalns=None, localns=None):
+        """Return type hints for a function or method object.
+
+        This is often the same as obj.__annotations__, but it handles
+        forward references encoded as string literals, and if necessary
+        adds Optional[t] if a default value equal to None is set.
+
+        BEWARE -- the behavior of globalns and localns is counterintuitive
+        (unless you are familiar with how eval() and exec() work).  The
+        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.
+
+        - If one dict argument is passed, it is used for both globals and
+          locals.
+
+        - If two dict arguments are passed, they specify globals and
+          locals, respectively.
+        """
+        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
         defaults = _get_defaults(obj)
-        hints = obj.__annotations__
+        hints = dict(obj.__annotations__)
         for name, value in hints.items():
-            if value is None:
-                value = type(None)
             if isinstance(value, str):
                 value = _ForwardRef(value)
             value = _eval_type(value, globalns, localns)
@@ -1259,62 +1342,30 @@
             hints[name] = value
         return hints
 
-    if isinstance(obj, types.ModuleType):
-        try:
-            hints = obj.__annotations__
-        except AttributeError:
-            return {}
-        # we keep only those annotations that can be accessed on module
-        members = obj.__dict__
-        hints = {name: value for name, value in hints.items()
-                                          if name in members}
-        for name, value in hints.items():
-            if value is None:
-                value = type(None)
-            if isinstance(value, str):
-                value = _ForwardRef(value)
-            value = _eval_type(value, globalns, localns)
-            hints[name] = value
-        return hints
-
-    if isinstance(object, type):
-        cmap = None
-        for base in reversed(obj.__mro__):
-            new_map = collections.ChainMap if cmap is None else cmap.new_child
-            try:
-                hints = base.__dict__['__annotations__']
-            except KeyError:
-                cmap = new_map()
-            else:
-                for name, value in hints.items():
-                    if value is None:
-                        value = type(None)
-                    if isinstance(value, str):
-                        value = _ForwardRef(value)
-                    value = _eval_type(value, globalns, localns)
-                    hints[name] = value
-                cmap = new_map(hints)
-        return cmap
-
-    raise TypeError('{!r} is not a module, class, method, '
-                    'or function.'.format(obj))
-
 
 def no_type_check(arg):
     """Decorator to indicate that annotations are not type hints.
 
     The argument must be a class or function; if it is a class, it
-    applies recursively to all methods defined in that class (but not
-    to methods defined in its superclasses or subclasses).
+    applies recursively to all methods and classes defined in that class
+    (but not to methods defined in its superclasses or subclasses).
 
-    This mutates the function(s) in place.
+    This mutates the function(s) or class(es) in place.
     """
     if isinstance(arg, type):
-        for obj in arg.__dict__.values():
+        arg_attrs = arg.__dict__.copy()
+        for attr, val in arg.__dict__.items():
+            if val in arg.__bases__:
+                arg_attrs.pop(attr)
+        for obj in arg_attrs.values():
             if isinstance(obj, types.FunctionType):
                 obj.__no_type_check__ = True
-    else:
+            if isinstance(obj, type):
+                no_type_check(obj)
+    try:
         arg.__no_type_check__ = True
+    except TypeError: # built-in classes
+        pass
     return arg
 
 
@@ -1725,7 +1776,7 @@
 
 
 # This is not a real generic class.  Don't use outside annotations.
-class Type(type, Generic[CT_co], extra=type):
+class Type(Generic[CT_co], extra=type):
     """A special construct usable to annotate class objects.
 
     For example, suppose we have the following classes::
@@ -1750,31 +1801,66 @@
     """
 
 
-def NamedTuple(typename, fields):
-    """Typed version of namedtuple.
-
-    Usage::
-
-        Employee = typing.NamedTuple('Employee', [('name', str), 'id', int)])
-
-    This is equivalent to::
-
-        Employee = collections.namedtuple('Employee', ['name', 'id'])
-
-    The resulting class has one extra attribute: _field_types,
-    giving a dict mapping field names to types.  (The field names
-    are in the _fields attribute, which is part of the namedtuple
-    API.)
-    """
-    fields = [(n, t) for n, t in fields]
-    cls = collections.namedtuple(typename, [n for n, t in fields])
-    cls._field_types = dict(fields)
-    # Set the module to the caller's module (otherwise it'd be 'typing').
+def _make_nmtuple(name, types):
+    nm_tpl = collections.namedtuple(name, [n for n, t in types])
+    nm_tpl._field_types = dict(types)
     try:
-        cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
+        nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
     except (AttributeError, ValueError):
         pass
-    return cls
+    return nm_tpl
+
+
+if sys.version_info[:2] >= (3, 6):
+    class NamedTupleMeta(type):
+
+        def __new__(cls, typename, bases, ns, *, _root=False):
+            if _root:
+                return super().__new__(cls, typename, bases, ns)
+            types = ns.get('__annotations__', {})
+            return _make_nmtuple(typename, types.items())
+
+    class NamedTuple(metaclass=NamedTupleMeta, _root=True):
+        """Typed version of namedtuple.
+
+        Usage::
+
+            class Employee(NamedTuple):
+                name: str
+                id: int
+
+        This is equivalent to::
+
+            Employee = collections.namedtuple('Employee', ['name', 'id'])
+
+        The resulting class has one extra attribute: _field_types,
+        giving a dict mapping field names to types.  (The field names
+        are in the _fields attribute, which is part of the namedtuple
+        API.) Backward-compatible usage::
+
+            Employee = NamedTuple('Employee', [('name', str), ('id', int)])
+        """
+
+        def __new__(self, typename, fields):
+            return _make_nmtuple(typename, fields)
+else:
+    def NamedTuple(typename, fields):
+        """Typed version of namedtuple.
+
+        Usage::
+
+            Employee = typing.NamedTuple('Employee', [('name', str), 'id', int)])
+
+        This is equivalent to::
+
+            Employee = collections.namedtuple('Employee', ['name', 'id'])
+
+        The resulting class has one extra attribute: _field_types,
+        giving a dict mapping field names to types.  (The field names
+        are in the _fields attribute, which is part of the namedtuple
+        API.)
+        """
+        return _make_nmtuple(typename, fields)
 
 
 def NewType(name, tp):

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list