summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGuido van Rossum <guido@dropbox.com>2016-09-11 22:34:56 (GMT)
committerGuido van Rossum <guido@dropbox.com>2016-09-11 22:34:56 (GMT)
commit0a6976da105a5be76a6a342c7eb978c82b2fd4d2 (patch)
treed3d60fc2ca6ed86b1705a8edbbe6f2cfd64fbb8e /Lib
parent0e0cfd71355accafd009916472ebb4d4fe0ab4ec (diff)
downloadcpython-0a6976da105a5be76a6a342c7eb978c82b2fd4d2.zip
cpython-0a6976da105a5be76a6a342c7eb978c82b2fd4d2.tar.gz
cpython-0a6976da105a5be76a6a342c7eb978c82b2fd4d2.tar.bz2
Issue #28079: Update typing and test typing from python/typing repo.
Ivan Levkivskyi (3.5 version)
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_typing.py142
-rw-r--r--Lib/typing.py328
2 files changed, 407 insertions, 63 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 72afe67..3b99060 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -9,9 +9,9 @@ from typing import Any
from typing import TypeVar, AnyStr
from typing import T, KT, VT # Not in __all__.
from typing import Union, Optional
-from typing import Tuple
+from typing import Tuple, List
from typing import Callable
-from typing import Generic
+from typing import Generic, ClassVar
from typing import cast
from typing import get_type_hints
from typing import no_type_check, no_type_check_decorator
@@ -827,6 +827,43 @@ class GenericTests(BaseTestCase):
with self.assertRaises(Exception):
D[T]
+class ClassVarTests(BaseTestCase):
+
+ def test_basics(self):
+ with self.assertRaises(TypeError):
+ ClassVar[1]
+ with self.assertRaises(TypeError):
+ ClassVar[int, str]
+ with self.assertRaises(TypeError):
+ ClassVar[int][str]
+
+ def test_repr(self):
+ self.assertEqual(repr(ClassVar), 'typing.ClassVar')
+ cv = ClassVar[int]
+ self.assertEqual(repr(cv), 'typing.ClassVar[int]')
+ cv = ClassVar[Employee]
+ self.assertEqual(repr(cv), 'typing.ClassVar[%s.Employee]' % __name__)
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(ClassVar)):
+ pass
+ with self.assertRaises(TypeError):
+ class C(type(ClassVar[int])):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ type(ClassVar)()
+ with self.assertRaises(TypeError):
+ type(ClassVar[Optional[int]])()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, ClassVar[int])
+ with self.assertRaises(TypeError):
+ issubclass(int, ClassVar)
+
class VarianceTests(BaseTestCase):
@@ -1119,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):
@@ -1426,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):
@@ -1463,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 5573a1f..4676d28 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -10,6 +10,8 @@ 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.
@@ -17,6 +19,7 @@ __all__ = [
# Super-special typing primitives.
'Any',
'Callable',
+ 'ClassVar',
'Generic',
'Optional',
'Tuple',
@@ -270,7 +273,7 @@ class _TypeAlias:
def _get_type_vars(types, tvars):
for t in types:
- if isinstance(t, TypingMeta):
+ if isinstance(t, TypingMeta) or isinstance(t, _ClassVar):
t._get_type_vars(tvars)
@@ -281,7 +284,7 @@ def _type_vars(types):
def _eval_type(t, globalns, localns):
- if isinstance(t, TypingMeta):
+ if isinstance(t, TypingMeta) or isinstance(t, _ClassVar):
return t._eval_type(globalns, localns)
else:
return t
@@ -1114,6 +1117,67 @@ class Generic(metaclass=GenericMeta):
return obj
+class _ClassVar(metaclass=TypingMeta, _root=True):
+ """Special type construct to mark class variables.
+
+ An annotation wrapped in ClassVar indicates that a given
+ attribute is intended to be used as a class variable and
+ should not be set on instances of that class. Usage::
+
+ class Starship:
+ stats: ClassVar[Dict[str, int]] = {} # class variable
+ damage: int = 10 # instance variable
+
+ ClassVar accepts only types and cannot be further subscribed.
+
+ Note that ClassVar is not a class itself, and should not
+ be used with isinstance() or issubclass().
+ """
+
+ def __init__(self, tp=None, _root=False):
+ cls = type(self)
+ if _root:
+ self.__type__ = tp
+ else:
+ raise TypeError('Cannot initialize {}'.format(cls.__name__[1:]))
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(_type_check(item,
+ '{} accepts only types.'.format(cls.__name__[1:])),
+ _root=True)
+ raise TypeError('{} cannot be further subscripted'
+ .format(cls.__name__[1:]))
+
+ def _eval_type(self, globalns, localns):
+ return type(self)(_eval_type(self.__type__, globalns, localns),
+ _root=True)
+
+ def _get_type_vars(self, tvars):
+ if self.__type__:
+ _get_type_vars(self.__type__, tvars)
+
+ def __repr__(self):
+ cls = type(self)
+ if not self.__type__:
+ return '{}.{}'.format(cls.__module__, cls.__name__[1:])
+ return '{}.{}[{}]'.format(cls.__module__, cls.__name__[1:],
+ _type_repr(self.__type__))
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _ClassVar):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ClassVar = _ClassVar(_root=True)
+
+
def cast(typ, val):
"""Cast a value to a type.
@@ -1141,62 +1205,167 @@ def _get_defaults(func):
return res
-def get_type_hints(obj, globalns=None, localns=None):
- """Return type hints for a function or method 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.
- 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.
+ 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.
- - 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.
+ 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.
- - If one dict argument is passed, it is used for both globals and
- locals.
+ 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 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 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
+
+ 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
- elif localns is None:
- localns = globalns
- defaults = _get_defaults(obj)
- hints = dict(obj.__annotations__)
- for name, value in hints.items():
- 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
+ defaults = _get_defaults(obj)
+ hints = dict(obj.__annotations__)
+ for name, value in hints.items():
+ 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
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
@@ -1300,6 +1469,8 @@ class _ProtocolMeta(GenericMeta):
else:
if (not attr.startswith('_abc_') and
attr != '__abstractmethods__' and
+ attr != '__annotations__' and
+ attr != '__weakref__' and
attr != '_is_protocol' and
attr != '__dict__' and
attr != '__args__' and
@@ -1605,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::
@@ -1630,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):