summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGuido van Rossum <guido@dropbox.com>2016-09-11 22:31:27 (GMT)
committerGuido van Rossum <guido@dropbox.com>2016-09-11 22:31:27 (GMT)
commit7ac1f7d269acf4f0153c3ddfa2e578bd4bf16744 (patch)
treeac9c1907adfe71d88a54a13b367cc15ea9c59275 /Lib
parent5fe668c6727b1301d4fbeb151d81854e74431295 (diff)
downloadcpython-7ac1f7d269acf4f0153c3ddfa2e578bd4bf16744.zip
cpython-7ac1f7d269acf4f0153c3ddfa2e578bd4bf16744.tar.gz
cpython-7ac1f7d269acf4f0153c3ddfa2e578bd4bf16744.tar.bz2
Issue #28079: Update typing and test typing from python/typing repo.
Ivan Levkivskyi (3.6 version)
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_typing.py143
-rw-r--r--Lib/typing.py290
2 files changed, 289 insertions, 144 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index e3904b1..3b99060 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -4,8 +4,6 @@ import pickle
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 @@ class ForwardRefTests(BaseTestCase):
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 @@ class AsyncIteratorWrapper(typing.AsyncIterator[T_a]):
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 @@ class TypeTests(BaseTestCase):
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 @@ class NamedTupleTests(BaseTestCase):
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
index 6ce74fc..4676d28 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -6,11 +6,12 @@ import functools
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 @@ def _get_defaults(func):
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 @@ def get_type_hints(obj, globalns=None, localns=None):
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 @@ CT_co = TypeVar('CT_co', covariant=True, bound=type)
# 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 @@ class Type(type, Generic[CT_co], extra=type):
"""
-def NamedTuple(typename, fields):
- """Typed version of namedtuple.
+def _make_nmtuple(name, types):
+ nm_tpl = collections.namedtuple(name, [n for n, t in types])
+ nm_tpl._field_types = dict(types)
+ try:
+ nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ pass
+ return nm_tpl
- Usage::
- Employee = typing.NamedTuple('Employee', [('name', str), 'id', int)])
+if sys.version_info[:2] >= (3, 6):
+ class NamedTupleMeta(type):
- This is equivalent to::
+ 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())
- Employee = collections.namedtuple('Employee', ['name', 'id'])
+ class NamedTuple(metaclass=NamedTupleMeta, _root=True):
+ """Typed version of namedtuple.
- 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').
- try:
- cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
- except (AttributeError, ValueError):
- pass
- return cls
+ 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):