diff -r 1c2a4f29e1ab Lib/test/test_typing.py --- a/Lib/test/test_typing.py Sun Sep 11 09:45:24 2016 -0700 +++ b/Lib/test/test_typing.py Sun Sep 11 19:13:11 2016 +0200 @@ -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 -r 1c2a4f29e1ab Lib/typing.py --- a/Lib/typing.py Sun Sep 11 09:45:24 2016 -0700 +++ b/Lib/typing.py Sun Sep 11 19:13:11 2016 +0200 @@ -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):