diff options
-rw-r--r-- | Lib/test/test_typing.py | 142 | ||||
-rw-r--r-- | Lib/typing.py | 328 |
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): |