Issue #28079: Update typing and test typing from python/typing repo.

Ivan Levkivskyi (3.6 version)
This commit is contained in:
Guido van Rossum 2016-09-11 15:31:27 -07:00
parent 5fe668c672
commit 7ac1f7d269
2 changed files with 291 additions and 146 deletions

View file

@ -4,8 +4,6 @@ import pickle
import re import re
import sys import sys
from unittest import TestCase, main, skipUnless, SkipTest 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 Any
from typing import TypeVar, AnyStr from typing import TypeVar, AnyStr
@ -969,46 +967,6 @@ class ForwardRefTests(BaseTestCase):
right_hints = get_type_hints(t.add_right, globals(), locals()) right_hints = get_type_hints(t.add_right, globals(), locals())
self.assertEqual(right_hints['node'], Optional[Node[T]]) 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): def test_forwardref_instance_type_error(self):
fr = typing._ForwardRef('int') fr = typing._ForwardRef('int')
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
@ -1198,6 +1156,84 @@ class AsyncIteratorWrapper(typing.AsyncIterator[T_a]):
if PY35: if PY35:
exec(PY35_TESTS) 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): class CollectionsAbcTests(BaseTestCase):
@ -1505,6 +1541,18 @@ class TypeTests(BaseTestCase):
joe = new_user(BasicUser) 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): class NewTypeTests(BaseTestCase):
@ -1542,6 +1590,17 @@ class NamedTupleTests(BaseTestCase):
self.assertEqual(Emp._fields, ('name', 'id')) self.assertEqual(Emp._fields, ('name', 'id'))
self.assertEqual(Emp._field_types, dict(name=str, id=int)) 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): def test_pickle(self):
global Emp # pickle wants to reference the class by name global Emp # pickle wants to reference the class by name
Emp = NamedTuple('Emp', [('name', str), ('id', int)]) Emp = NamedTuple('Emp', [('name', str), ('id', int)])

View file

@ -6,11 +6,12 @@ import functools
import re as stdlib_re # Avoid confusion with the re we export. import re as stdlib_re # Avoid confusion with the re we export.
import sys import sys
import types import types
try: try:
import collections.abc as collections_abc import collections.abc as collections_abc
except ImportError: except ImportError:
import collections as collections_abc # Fallback for PY3.2. 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. # Please keep __all__ alphabetized within each category.
@ -1204,53 +1205,135 @@ def _get_defaults(func):
return res return res
def get_type_hints(obj, globalns=None, localns=None): if sys.version_info[:2] >= (3, 3):
"""Return type hints for an object. 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 This is often the same as obj.__annotations__, but it handles
forward references encoded as string literals, and if necessary forward references encoded as string literals, and if necessary
adds Optional[t] if a default value equal to None is set. adds Optional[t] if a default value equal to None is set.
The argument may be a module, class, method, or function. The annotations 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 are returned as a dictionary, or in the case of a class, a ChainMap of
dictionaries. dictionaries.
TypeError is raised if the argument is not of a type that can contain 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 annotations, and an empty dictionary is returned if no annotations are
present. present.
BEWARE -- the behavior of globalns and localns is counterintuitive BEWARE -- the behavior of globalns and localns is counterintuitive
(unless you are familiar with how eval() and exec() work). The (unless you are familiar with how eval() and exec() work). The
search order is locals first, then globals. search order is locals first, then globals.
- If no dict arguments are passed, an attempt is made to use the - 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 globals from obj, and these are also used as the locals. If the
object does not appear to have globals, an exception is raised. object does not appear to have globals, an exception is raised.
- If one dict argument is passed, it is used for both globals and - If one dict argument is passed, it is used for both globals and
locals. locals.
- If two dict arguments are passed, they specify globals and - If two dict arguments are passed, they specify globals and
locals, respectively. locals, respectively.
""" """
if getattr(obj, '__no_type_check__', None): if getattr(obj, '__no_type_check__', None):
return {} return {}
if globalns is None: if globalns is None:
globalns = getattr(obj, '__globals__', {}) globalns = getattr(obj, '__globals__', {})
if localns is None: if localns is None:
localns = globalns
elif localns is None:
localns = globalns localns = globalns
elif localns is None:
localns = globalns
if (isinstance(obj, types.FunctionType) or if (isinstance(obj, types.FunctionType) or
isinstance(obj, types.BuiltinFunctionType) or isinstance(obj, types.BuiltinFunctionType) or
isinstance(obj, types.MethodType)): 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) defaults = _get_defaults(obj)
hints = obj.__annotations__ hints = dict(obj.__annotations__)
for name, value in hints.items(): for name, value in hints.items():
if value is None:
value = type(None)
if isinstance(value, str): if isinstance(value, str):
value = _ForwardRef(value) value = _ForwardRef(value)
value = _eval_type(value, globalns, localns) value = _eval_type(value, globalns, localns)
@ -1259,62 +1342,30 @@ def get_type_hints(obj, globalns=None, localns=None):
hints[name] = value hints[name] = value
return hints 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): def no_type_check(arg):
"""Decorator to indicate that annotations are not type hints. """Decorator to indicate that annotations are not type hints.
The argument must be a class or function; if it is a class, it 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 applies recursively to all methods and classes defined in that class
to methods defined in its superclasses or subclasses). (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): 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): if isinstance(obj, types.FunctionType):
obj.__no_type_check__ = True obj.__no_type_check__ = True
else: if isinstance(obj, type):
no_type_check(obj)
try:
arg.__no_type_check__ = True arg.__no_type_check__ = True
except TypeError: # built-in classes
pass
return arg 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. # 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. """A special construct usable to annotate class objects.
For example, suppose we have the following classes:: For example, suppose we have the following classes::
@ -1750,31 +1801,66 @@ class Type(type, Generic[CT_co], extra=type):
""" """
def NamedTuple(typename, fields): def _make_nmtuple(name, types):
"""Typed version of namedtuple. nm_tpl = collections.namedtuple(name, [n for n, t in types])
nm_tpl._field_types = dict(types)
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').
try: 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): except (AttributeError, ValueError):
pass 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): def NewType(name, tp):