From d20c39e025ccbac71be4edf1b120184fd4fbb320 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 10:34:52 +0200 Subject: [PATCH 01/13] Copy typing upstream (including ClassVar for Python2) --- python2/test_typing.py | 39 ++++++++++++++++++- python2/typing.py | 66 +++++++++++++++++++++++++++++++- src/test/ann_module.py | 53 ++++++++++++++++++++++++++ src/test/ann_module2.py | 36 ++++++++++++++++++ src/test/ann_module3.py | 18 +++++++++ src/test_typing.py | 83 ++++++++++++++++++++++++++++++++++++++++- src/typing.py | 68 ++++++++++++++++++++++++++++++++- 7 files changed, 356 insertions(+), 7 deletions(-) create mode 100644 src/test/ann_module.py create mode 100644 src/test/ann_module2.py create mode 100644 src/test/ann_module3.py diff --git a/python2/test_typing.py b/python2/test_typing.py index d72a58964..13509d4ee 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -12,7 +12,7 @@ from typing import Union, Optional from typing import Tuple from typing import Callable -from typing import Generic +from typing import Generic, ClassVar from typing import cast from typing import Type from typing import NewType @@ -802,6 +802,43 @@ class D(C): 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): diff --git a/python2/typing.py b/python2/typing.py index 03632eaf0..feb7e12c6 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -18,6 +18,7 @@ # Super-special typing primitives. 'Any', 'Callable', + 'ClassVar', 'Generic', 'Optional', 'Tuple', @@ -265,7 +266,7 @@ def __subclasscheck__(self, cls): 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) @@ -276,7 +277,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 @@ -1132,6 +1133,67 @@ def __new__(cls, *args, **kwds): 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. diff --git a/src/test/ann_module.py b/src/test/ann_module.py new file mode 100644 index 000000000..9e6b87dac --- /dev/null +++ b/src/test/ann_module.py @@ -0,0 +1,53 @@ + + +""" +The module for testing variable annotations. +Empty lines above are for good reason (testing for correct line numbers) +""" + +from typing import Optional + +__annotations__[1] = 2 + +class C: + + x = 5; y: Optional['C'] = None + +from typing import Tuple +x: int = 5; y: str = x; f: Tuple[int, int] + +class M(type): + + __annotations__['123'] = 123 + o: type = object + +(pars): bool = True + +class D(C): + j: str = 'hi'; k: str= 'bye' + +from types import new_class +h_class = new_class('H', (C,)) +j_class = new_class('J') + +class F(): + z: int = 5 + def __init__(self, x): + pass + +class Y(F): + def __init__(self): + super(F, self).__init__(123) + +class Meta(type): + def __new__(meta, name, bases, namespace): + return super().__new__(meta, name, bases, namespace) + +class S(metaclass = Meta): + x: str = 'something' + y: str = 'something else' + +def foo(x: int = 10): + def bar(y: List[str]): + x: str = 'yes' + bar() diff --git a/src/test/ann_module2.py b/src/test/ann_module2.py new file mode 100644 index 000000000..76cf5b3ad --- /dev/null +++ b/src/test/ann_module2.py @@ -0,0 +1,36 @@ +""" +Some correct syntax for variable annotation here. +More examples are in test_grammar and test_parser. +""" + +from typing import no_type_check, ClassVar + +i: int = 1 +j: int +x: float = i/10 + +def f(): + class C: ... + return C() + +f().new_attr: object = object() + +class C: + def __init__(self, x: int) -> None: + self.x = x + +c = C(5) +c.new_attr: int = 10 + +__annotations__ = {} + + +@no_type_check +class NTC: + def meth(self, param: complex) -> None: + ... + +class CV: + var: ClassVar['CV'] + +CV.var = CV() diff --git a/src/test/ann_module3.py b/src/test/ann_module3.py new file mode 100644 index 000000000..eccd7be22 --- /dev/null +++ b/src/test/ann_module3.py @@ -0,0 +1,18 @@ +""" +Correct syntax for variable annotation that should fail at runtime +in a certain manner. More examples are in test_grammar and test_parser. +""" + +def f_bad_ann(): + __annotations__[1] = 2 + +class C_OK: + def __init__(self, x: int) -> None: + self.x: no_such_name = x # This one is OK as proposed by Guido + +class D_bad_ann: + def __init__(self, x: int) -> None: + sfel.y: int = 0 + +def g_bad_ann(): + no_such_name.attr: int = 0 diff --git a/src/test_typing.py b/src/test_typing.py index 1f5e72fe2..4d720f98c 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -4,14 +4,16 @@ 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 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 +829,43 @@ class D(C): 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): @@ -930,6 +969,46 @@ def add_right(self, node: 'Node[T]' = None): 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): diff --git a/src/typing.py b/src/typing.py index b628ba3dc..6d3f70c93 100644 --- a/src/typing.py +++ b/src/typing.py @@ -17,6 +17,7 @@ # Super-special typing primitives. 'Any', 'Callable', + 'ClassVar', 'Generic', 'Optional', 'Tuple', @@ -270,7 +271,7 @@ def __subclasscheck__(self, cls): 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 +282,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 +1115,67 @@ def __new__(cls, *args, **kwds): 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. @@ -1300,6 +1362,8 @@ def _get_protocol_attrs(self): 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 From 5d1944834c4959e4a047b8b5b4a9c15c974c1f7c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 10:38:55 +0200 Subject: [PATCH 02/13] update docstring for Python2 --- python2/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python2/typing.py b/python2/typing.py index feb7e12c6..3729b483e 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1141,8 +1141,8 @@ class _ClassVar(metaclass=TypingMeta, _root=True): should not be set on instances of that class. Usage:: class Starship: - stats: ClassVar[Dict[str, int]] = {} # class variable - damage: int = 10 # instance variable + stats = {} # type: ClassVar[Dict[str, int]] # class variable + damage = 10 # type: int # instance variable ClassVar accepts only types and cannot be further subscribed. From 69ff01986b504b146be89d3ff8c7b22dcef06742 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 10:51:51 +0200 Subject: [PATCH 03/13] Make Python3 tests backward compatible --- src/test_typing.py | 57 ++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 4d720f98c..8e5067629 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -5,7 +5,9 @@ import sys from unittest import TestCase, main, skipUnless, SkipTest from collections import ChainMap -from test import ann_module, ann_module2, ann_module3 +if sys.version_info[:2] >= (3, 6): + from test import ann_module, ann_module2, ann_module3 +from textwrap import dedent from typing import Any from typing import TypeVar, AnyStr @@ -971,30 +973,32 @@ def add_right(self, node: 'Node[T]' = None): 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}) + if sys.version_info[:2] >= (3, 6): + 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}) + self.assertEqual(gth(ann_module2.NTC.meth), {}) def testf(x, y): ... testf.__annotations__['x'] = 'int' self.assertEqual(gth(testf), {'x': int}) - self.assertEqual(gth(ann_module2.NTC.meth), {}) # interactions with ClassVar + stmt = dedent("""\ class B: x: ClassVar[Optional['B']] = None y: int @@ -1002,12 +1006,15 @@ 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]]},{},{})) + """) + if sys.version_info[:2] >= (3, 6): + exec(stmt) + 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') From 66921dc58fd21f2b02fe28f88f5f7a12a8c38b15 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:04:19 +0200 Subject: [PATCH 04/13] Fixed metaclass for Python2 --- python2/typing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python2/typing.py b/python2/typing.py index 3729b483e..c6a26ebea 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1133,7 +1133,7 @@ def __new__(cls, *args, **kwds): return obj -class _ClassVar(metaclass=TypingMeta, _root=True): +class _ClassVar(object): """Special type construct to mark class variables. An annotation wrapped in ClassVar indicates that a given @@ -1150,6 +1150,8 @@ class Starship: be used with isinstance() or issubclass(). """ + __metaclass__ = TypingMeta + def __init__(self, tp=None, _root=False): cls = type(self) if _root: From 5a706c00622a7d36afc579ac02b6f77f9fec1b6a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:24:36 +0200 Subject: [PATCH 05/13] Fixed ChainMap also in tpying for 3.2 --- src/test_typing.py | 2 +- src/typing.py | 165 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 133 insertions(+), 34 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 8e5067629..3c504824e 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -4,9 +4,9 @@ import re import sys from unittest import TestCase, main, skipUnless, SkipTest -from collections import ChainMap if sys.version_info[:2] >= (3, 6): from test import ann_module, ann_module2, ann_module3 + from collections import ChainMap from textwrap import dedent from typing import Any diff --git a/src/typing.py b/src/typing.py index 6d3f70c93..02d7e9d6e 100644 --- a/src/typing.py +++ b/src/typing.py @@ -10,6 +10,8 @@ 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. @@ -1203,45 +1205,142 @@ 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): From 69a45026d11516543bb7b5abff048b3633991f7f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:38:59 +0200 Subject: [PATCH 06/13] ClassVar must be defined before Any in Python2 --- python2/typing.py | 126 +++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/python2/typing.py b/python2/typing.py index c6a26ebea..0bdd390b9 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -321,6 +321,69 @@ def _type_repr(obj): return repr(obj) +class _ClassVar(object): + """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 = {} # type: ClassVar[Dict[str, int]] # class variable + damage = 10 # type: int # 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(). + """ + + __metaclass__ = TypingMeta + + 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) + + class AnyMeta(TypingMeta): """Metaclass for Any.""" @@ -1133,69 +1196,6 @@ def __new__(cls, *args, **kwds): return obj -class _ClassVar(object): - """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 = {} # type: ClassVar[Dict[str, int]] # class variable - damage = 10 # type: int # 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(). - """ - - __metaclass__ = TypingMeta - - 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. From 0b9092d21208f1b1b6b3df0bbbc31fbbcbaabeac Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:51:01 +0200 Subject: [PATCH 07/13] Attempt at prohibiting subclasses of ClassVar in Python2 --- python2/typing.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python2/typing.py b/python2/typing.py index 0bdd390b9..6a011d479 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -321,6 +321,15 @@ def _type_repr(obj): return repr(obj) +class ClassVarMeta(TypingMeta): + """Metaclass for _ClassVar""" + + def __new__(cls, name, bases, namespace): + cls.assert_no_subclassing(bases) + self = super(ClassVarMeta, cls).__new__(cls, name, bases, namespace) + return self + + class _ClassVar(object): """Special type construct to mark class variables. @@ -338,7 +347,7 @@ class Starship: be used with isinstance() or issubclass(). """ - __metaclass__ = TypingMeta + __metaclass__ = ClassVarMeta def __init__(self, tp=None, _root=False): cls = type(self) From 7089f66512175f389d8c14016bdd278af88fb6be Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:55:25 +0200 Subject: [PATCH 08/13] Try alternative location for test modules --- src/{test => }/ann_module.py | 0 src/{test => }/ann_module2.py | 0 src/{test => }/ann_module3.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/{test => }/ann_module.py (100%) rename src/{test => }/ann_module2.py (100%) rename src/{test => }/ann_module3.py (100%) diff --git a/src/test/ann_module.py b/src/ann_module.py similarity index 100% rename from src/test/ann_module.py rename to src/ann_module.py diff --git a/src/test/ann_module2.py b/src/ann_module2.py similarity index 100% rename from src/test/ann_module2.py rename to src/ann_module2.py diff --git a/src/test/ann_module3.py b/src/ann_module3.py similarity index 100% rename from src/test/ann_module3.py rename to src/ann_module3.py From c43cac5ebd618b0f1e3ae5877dd35bf146758844 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 11:57:49 +0200 Subject: [PATCH 09/13] Corrected import --- src/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing.py b/src/test_typing.py index 3c504824e..c2a572bf7 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -5,7 +5,7 @@ import sys from unittest import TestCase, main, skipUnless, SkipTest if sys.version_info[:2] >= (3, 6): - from test import ann_module, ann_module2, ann_module3 + import ann_module, ann_module2, ann_module3 from collections import ChainMap from textwrap import dedent From 37f14ad6463b0845903c3e7a6c09ffff53dd069b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 12:07:49 +0200 Subject: [PATCH 10/13] Remove test modules --- src/ann_module.py | 53 ---------------------------------------------- src/ann_module2.py | 36 ------------------------------- src/ann_module3.py | 18 ---------------- src/test_typing.py | 2 +- 4 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 src/ann_module.py delete mode 100644 src/ann_module2.py delete mode 100644 src/ann_module3.py diff --git a/src/ann_module.py b/src/ann_module.py deleted file mode 100644 index 9e6b87dac..000000000 --- a/src/ann_module.py +++ /dev/null @@ -1,53 +0,0 @@ - - -""" -The module for testing variable annotations. -Empty lines above are for good reason (testing for correct line numbers) -""" - -from typing import Optional - -__annotations__[1] = 2 - -class C: - - x = 5; y: Optional['C'] = None - -from typing import Tuple -x: int = 5; y: str = x; f: Tuple[int, int] - -class M(type): - - __annotations__['123'] = 123 - o: type = object - -(pars): bool = True - -class D(C): - j: str = 'hi'; k: str= 'bye' - -from types import new_class -h_class = new_class('H', (C,)) -j_class = new_class('J') - -class F(): - z: int = 5 - def __init__(self, x): - pass - -class Y(F): - def __init__(self): - super(F, self).__init__(123) - -class Meta(type): - def __new__(meta, name, bases, namespace): - return super().__new__(meta, name, bases, namespace) - -class S(metaclass = Meta): - x: str = 'something' - y: str = 'something else' - -def foo(x: int = 10): - def bar(y: List[str]): - x: str = 'yes' - bar() diff --git a/src/ann_module2.py b/src/ann_module2.py deleted file mode 100644 index 76cf5b3ad..000000000 --- a/src/ann_module2.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Some correct syntax for variable annotation here. -More examples are in test_grammar and test_parser. -""" - -from typing import no_type_check, ClassVar - -i: int = 1 -j: int -x: float = i/10 - -def f(): - class C: ... - return C() - -f().new_attr: object = object() - -class C: - def __init__(self, x: int) -> None: - self.x = x - -c = C(5) -c.new_attr: int = 10 - -__annotations__ = {} - - -@no_type_check -class NTC: - def meth(self, param: complex) -> None: - ... - -class CV: - var: ClassVar['CV'] - -CV.var = CV() diff --git a/src/ann_module3.py b/src/ann_module3.py deleted file mode 100644 index eccd7be22..000000000 --- a/src/ann_module3.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Correct syntax for variable annotation that should fail at runtime -in a certain manner. More examples are in test_grammar and test_parser. -""" - -def f_bad_ann(): - __annotations__[1] = 2 - -class C_OK: - def __init__(self, x: int) -> None: - self.x: no_such_name = x # This one is OK as proposed by Guido - -class D_bad_ann: - def __init__(self, x: int) -> None: - sfel.y: int = 0 - -def g_bad_ann(): - no_such_name.attr: int = 0 diff --git a/src/test_typing.py b/src/test_typing.py index c2a572bf7..3c504824e 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -5,7 +5,7 @@ import sys from unittest import TestCase, main, skipUnless, SkipTest if sys.version_info[:2] >= (3, 6): - import ann_module, ann_module2, ann_module3 + from test import ann_module, ann_module2, ann_module3 from collections import ChainMap from textwrap import dedent From b1d3d1e3f844286361c8c95718110a3bf6a9fcc9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 9 Sep 2016 22:58:39 +0200 Subject: [PATCH 11/13] Restructured test_typing a bit. ann_modules still imported --- src/test_typing.py | 111 +++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 3c504824e..3d5b2dffa 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -4,10 +4,6 @@ import re import sys from unittest import TestCase, main, skipUnless, SkipTest -if sys.version_info[:2] >= (3, 6): - from test import ann_module, ann_module2, ann_module3 - from collections import ChainMap -from textwrap import dedent from typing import Any from typing import TypeVar, AnyStr @@ -971,51 +967,6 @@ def add_right(self, node: 'Node[T]' = None): 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 - if sys.version_info[:2] >= (3, 6): - 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}) - self.assertEqual(gth(ann_module2.NTC.meth), {}) - - def testf(x, y): ... - testf.__annotations__['x'] = 'int' - self.assertEqual(gth(testf), {'x': int}) - - # interactions with ClassVar - stmt = dedent("""\ - class B: - x: ClassVar[Optional['B']] = None - y: int - class C(B): - z: ClassVar['C'] = B() - class G(Generic[T]): - lst: ClassVar[List[T]] = [] - """) - if sys.version_info[:2] >= (3, 6): - exec(stmt) - 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): @@ -1205,6 +1156,68 @@ def __anext__(self) -> 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 C(B): + z: ClassVar['C'] = B() +class G(Generic[T]): + lst: ClassVar[List[T]] = [] +""" + +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): + self.assertEqual(gth(ann_module2.NTC.meth), {}) + + 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, 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]]},{},{})) + class CollectionsAbcTests(BaseTestCase): From 3c0810726b3d369ccc555d7c4834c45857d2a8d3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 10 Sep 2016 00:07:34 +0200 Subject: [PATCH 12/13] Fixed bugs after refoactorig tests --- src/test_typing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test_typing.py b/src/test_typing.py index 3d5b2dffa..4e184e654 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1165,8 +1165,8 @@ def __anext__(self) -> T_a: class B: x: ClassVar[Optional['B']] = None y: int -class C(B): - z: ClassVar['C'] = B() +class CSub(B): + z: ClassVar['CSub'] = B() class G(Generic[T]): lst: ClassVar[List[T]] = [] """ @@ -1211,10 +1211,10 @@ def testf(x, y): ... @skipUnless(PY36, 'Python 3.6 required') def test_get_type_hints_ClassVar(self): - self.assertEqual(gth(B, locals()), + self.assertEqual(gth(B, globals()), ChainMap({'y': int, 'x': ClassVar[Optional[B]]}, {})) - self.assertEqual(gth(C, locals()), - ChainMap({'z': ClassVar[C]}, + self.assertEqual(gth(CSub, globals()), + ChainMap({'z': ClassVar[CSub]}, {'y': int, 'x': ClassVar[Optional[B]]}, {})) self.assertEqual(gth(G), ChainMap({'lst': ClassVar[List[T]]},{},{})) From 33f55d7e67d7affd9f721cc0801f646eb7d4e59b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 10 Sep 2016 17:17:37 +0200 Subject: [PATCH 13/13] Trigger TRavis CI