From 46c47ab9697f43c5063f480b2a0139e71127fad4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 28 Jan 2017 17:51:04 +0100 Subject: [PATCH 1/5] Add more tests based on coverage report --- python2/test_typing.py | 38 +++++++++++++++++++++++++++++++ python2/typing.py | 26 ++++++++++++++++++--- src/test_typing.py | 51 ++++++++++++++++++++++++++++++++++++++++-- src/typing.py | 9 +++++--- 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/python2/test_typing.py b/python2/test_typing.py index 9240b9d3..2057a5f1 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -255,6 +255,12 @@ def test_repr(self): self.assertEqual(repr(u), 'typing.Union[%s.Employee, int]' % __name__) u = Union[int, Employee] self.assertEqual(repr(u), 'typing.Union[int, %s.Employee]' % __name__) + T = TypeVar('T') + u = Union[T, int][int] + self.assertEqual(repr(u), repr(int)) + u = Union[List[int], int] + self.assertEqual(repr(u), 'typing.Union[typing.List[int], int]') + def test_cannot_subclass(self): with self.assertRaises(TypeError): @@ -303,6 +309,14 @@ def test_union_instance_type_error(self): with self.assertRaises(TypeError): isinstance(42, Union[int, str]) + def test_no_eval_union(self): + u = Union[int, str] + self.assertIs(u._eval_type({}, {}), u) + + def test_function_repr_union(self): + def fun(): pass + self.assertEqual(repr(Union[fun, int]), 'typing.Union[fun, int]') + def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 A = Union[str, Pattern] @@ -853,6 +867,8 @@ def test_fail_with_bare_generic(self): Tuple[Generic[T]] with self.assertRaises(TypeError): List[typing._Protocol] + with self.assertRaises(TypeError): + isinstance(1, Generic) def test_type_erasure_special(self): T = TypeVar('T') @@ -1191,6 +1207,19 @@ def test_syntax_error(self): with self.assertRaises(SyntaxError): Generic['/T'] + def test_forwardref_subclass_type_error(self): + fr = typing._ForwardRef('int') + with self.assertRaises(TypeError): + issubclass(int, fr) + + def test_forward_equality(self): + fr = typing._ForwardRef('int') + self.assertEqual(fr, typing._ForwardRef('int')) + self.assertNotEqual(List['int'], List[int]) + + def test_forward_repr(self): + self.assertEqual(repr(List['int']), "typing.List[_ForwardRef(%r)]" % 'int') + class OverloadTests(BaseTestCase): @@ -1654,6 +1683,12 @@ def test_basics(self): Pattern[Union[str, bytes]] Match[Union[bytes, str]] + def test_alias_equality(self): + self.assertEqual(Pattern[str], Pattern[str]) + self.assertNotEqual(Pattern[str], Pattern[bytes]) + self.assertNotEqual(Pattern[str], Match[str]) + self.assertNotEqual(Pattern[str], str) + def test_errors(self): with self.assertRaises(TypeError): # Doesn't fit AnyStr. @@ -1668,6 +1703,9 @@ def test_errors(self): with self.assertRaises(TypeError): # We don't support isinstance(). isinstance(42, Pattern[str]) + with self.assertRaises(TypeError): + # We don't support issubclass(). + issubclass(Pattern[bytes], Pattern[str]) def test_repr(self): self.assertEqual(repr(Pattern), 'Pattern[~AnyStr]') diff --git a/python2/typing.py b/python2/typing.py index 9b08cf49..a4172f26 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -225,6 +225,15 @@ def _eval_type(self, globalns, localns): self.__forward_evaluated__ = True return self.__forward_value__ + def __eq__(self, other): + if not isinstance(other, _ForwardRef): + return NotImplemented + return (self.__forward_arg__ == other.__forward_arg__ and + self.__forward_value__ == other.__forward_value__) + + def __hash__(self): + return hash((self.__forward_arg__, self.__forward_value__)) + def __instancecheck__(self, obj): raise TypeError("Forward references cannot be used with isinstance().") @@ -281,6 +290,14 @@ def __getitem__(self, parameter): return self.__class__(self.name, parameter, self.impl_type, self.type_checker) + def __eq__(self, other): + if not isinstance(other, _TypeAlias): + return NotImplemented + return self.name == other.name and self.type_var == other.type_var + + def __hash__(self): + return hash((self.name, self.type_var)) + def __instancecheck__(self, obj): if not isinstance(self.type_var, TypeVar): raise TypeError("Parameterized type aliases cannot be used " @@ -1064,10 +1081,9 @@ def __new__(cls, name, bases, namespace, # with issubclass() and isinstance() in the same way as their # collections.abc counterparts (e.g., isinstance([], Iterable)). if ( - # allow overriding '__subclasshook__' not in namespace and extra or - hasattr(self.__subclasshook__, '__name__') and - self.__subclasshook__.__name__ == '__extrahook__' + # allow overriding + getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' ): self.__subclasshook__ = _make_subclasshook(self) @@ -1247,6 +1263,10 @@ def __new__(cls, *args, **kwds): return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) +# prevent class and instance checks against plain Generic +Generic.__subclasshook__ = _make_subclasshook(Generic) + + class _TypingEmpty(object): """Internal placeholder for () or []. Used by TupleMeta and CallableMeta to allow empty list/tuple in specific places, without allowing them diff --git a/src/test_typing.py b/src/test_typing.py index 266d9230..572b9491 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -258,6 +258,11 @@ def test_repr(self): self.assertEqual(repr(u), 'typing.Union[%s.Employee, int]' % __name__) u = Union[int, Employee] self.assertEqual(repr(u), 'typing.Union[int, %s.Employee]' % __name__) + T = TypeVar('T') + u = Union[T, int][int] + self.assertEqual(repr(u), repr(int)) + u = Union[List[int], int] + self.assertEqual(repr(u), 'typing.Union[typing.List[int], int]') def test_cannot_subclass(self): with self.assertRaises(TypeError): @@ -308,6 +313,15 @@ def test_union_instance_type_error(self): with self.assertRaises(TypeError): isinstance(42, Union[int, str]) + def test_no_eval_union(self): + u = Union[int, str] + def f(x: u): ... + self.assertIs(get_type_hints(f)['x'], u) + + def test_function_repr_union(self): + def fun() -> int: ... + self.assertEqual(repr(Union[fun, int]), 'typing.Union[fun, int]') + def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 A = Union[str, Pattern] @@ -892,6 +906,8 @@ def test_fail_with_bare_generic(self): Tuple[Generic[T]] with self.assertRaises(TypeError): List[typing._Protocol] + with self.assertRaises(TypeError): + isinstance(1, Generic) def test_type_erasure_special(self): T = TypeVar('T') @@ -1260,6 +1276,19 @@ def test_forwardref_instance_type_error(self): with self.assertRaises(TypeError): isinstance(42, fr) + def test_forwardref_subclass_type_error(self): + fr = typing._ForwardRef('int') + with self.assertRaises(TypeError): + issubclass(int, fr) + + def test_forward_equality(self): + fr = typing._ForwardRef('int') + self.assertEqual(fr, typing._ForwardRef('int')) + self.assertNotEqual(List['int'], List[int]) + + def test_forward_repr(self): + self.assertEqual(repr(List['int']), "typing.List[_ForwardRef('int')]") + def test_union_forward(self): def foo(a: Union['T']): @@ -1467,11 +1496,16 @@ class A: class B(A): x: ClassVar[Optional['B']] = None y: int + b: int class CSub(B): z: ClassVar['CSub'] = B() class G(Generic[T]): lst: ClassVar[List[T]] = [] +class NoneAndForward: + parent: 'NoneAndForward' + meaning: None + class CoolEmployee(NamedTuple): name: str cool: int @@ -1492,6 +1526,7 @@ def double(self): # fake names for the sake of static analysis ann_module = ann_module2 = ann_module3 = None A = B = CSub = G = CoolEmployee = CoolEmployeeWithDefault = XMeth = object + NoneAndForward = object gth = get_type_hints @@ -1526,6 +1561,8 @@ def test_get_type_hints_classes(self): {'y': Optional[ann_module.C]}) self.assertEqual(gth(ann_module.S), {'x': str, 'y': str}) self.assertEqual(gth(ann_module.foo), {'x': int}) + self.assertEqual(gth(NoneAndForward, globals()), + {'parent': NoneAndForward, 'meaning': type(None)}) @skipUnless(PY36, 'Python 3.6 required') def test_respect_no_type_check(self): @@ -1568,9 +1605,10 @@ def test_get_type_hints_ClassVar(self): self.assertEqual(gth(ann_module2.CV, ann_module2.__dict__), {'var': typing.ClassVar[ann_module2.CV]}) self.assertEqual(gth(B, globals()), - {'y': int, 'x': ClassVar[Optional[B]]}) + {'y': int, 'x': ClassVar[Optional[B]], 'b': int}) self.assertEqual(gth(CSub, globals()), - {'z': ClassVar[CSub], 'y': int, 'x': ClassVar[Optional[B]]}) + {'z': ClassVar[CSub], 'y': int, 'b': int, + 'x': ClassVar[Optional[B]]}) self.assertEqual(gth(G), {'lst': ClassVar[List[T]]}) @@ -2212,6 +2250,12 @@ def test_basics(self): Pattern[Union[str, bytes]] Match[Union[bytes, str]] + def test_alias_equality(self): + self.assertEqual(Pattern[str], Pattern[str]) + self.assertNotEqual(Pattern[str], Pattern[bytes]) + self.assertNotEqual(Pattern[str], Match[str]) + self.assertNotEqual(Pattern[str], str) + def test_errors(self): with self.assertRaises(TypeError): # Doesn't fit AnyStr. @@ -2226,6 +2270,9 @@ def test_errors(self): with self.assertRaises(TypeError): # We don't support isinstance(). isinstance(42, Pattern[str]) + with self.assertRaises(TypeError): + # We don't support issubclass(). + issubclass(Pattern[bytes], Pattern[str]) def test_repr(self): self.assertEqual(repr(Pattern), 'Pattern[~AnyStr]') diff --git a/src/typing.py b/src/typing.py index d3c9982d..37d9cc4c 100644 --- a/src/typing.py +++ b/src/typing.py @@ -994,10 +994,9 @@ def __new__(cls, name, bases, namespace, # with issubclass() and isinstance() in the same way as their # collections.abc counterparts (e.g., isinstance([], Iterable)). if ( - # allow overriding '__subclasshook__' not in namespace and extra or - hasattr(self.__subclasshook__, '__name__') and - self.__subclasshook__.__name__ == '__extrahook__' + # allow overriding + getattr(self.__subclasshook__, '__name__', '') == '__extrahook__' ): self.__subclasshook__ = _make_subclasshook(self) if isinstance(extra, abc.ABCMeta): @@ -1172,6 +1171,10 @@ def __new__(cls, *args, **kwds): return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) +# prevent class and instance checks against plain Generic +Generic.__subclasshook__ = _make_subclasshook(Generic) + + class _TypingEmpty: """Internal placeholder for () or []. Used by TupleMeta and CallableMeta to allow empty list/tuple in specific places, without allowing them From ddf5078f37fe73313b2b4da8ec17a3fa5355ebf6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 28 Jan 2017 18:03:44 +0100 Subject: [PATCH 2/5] Fix lint on PY2 --- python2/test_typing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python2/test_typing.py b/python2/test_typing.py index 2057a5f1..422ef5b4 100644 --- a/python2/test_typing.py +++ b/python2/test_typing.py @@ -261,7 +261,6 @@ def test_repr(self): u = Union[List[int], int] self.assertEqual(repr(u), 'typing.Union[typing.List[int], int]') - def test_cannot_subclass(self): with self.assertRaises(TypeError): class C(Union): From 43a1cb60dbf77b4c9d4fe4569193bbe18ad6ade4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 3 Feb 2017 23:40:55 +0100 Subject: [PATCH 3/5] Simplify PR to only tests and tiny code clean-up --- python2/typing.py | 4 ---- src/typing.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/python2/typing.py b/python2/typing.py index a4172f26..9a8e1d36 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1212,10 +1212,6 @@ def __copy__(self): self.__extra__, self.__orig_bases__) -# Prevent checks for Generic to crash when defining Generic. -Generic = None - - def _generic_new(base_cls, cls, *args, **kwds): # Assure type is erased on instantiation, # but attempt to store it in __orig_class__ diff --git a/src/typing.py b/src/typing.py index 37d9cc4c..aad2ced3 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1171,10 +1171,6 @@ def __new__(cls, *args, **kwds): return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) -# prevent class and instance checks against plain Generic -Generic.__subclasshook__ = _make_subclasshook(Generic) - - class _TypingEmpty: """Internal placeholder for () or []. Used by TupleMeta and CallableMeta to allow empty list/tuple in specific places, without allowing them From cf4e6a60437c8426e1e866b163bf6ecef7c7d156 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 3 Feb 2017 23:49:14 +0100 Subject: [PATCH 4/5] Remove _right_ code on PY2 --- python2/typing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python2/typing.py b/python2/typing.py index 9a8e1d36..d432fd78 100644 --- a/python2/typing.py +++ b/python2/typing.py @@ -1212,6 +1212,10 @@ def __copy__(self): self.__extra__, self.__orig_bases__) +# Prevent checks for Generic to crash when defining Generic. +Generic = None + + def _generic_new(base_cls, cls, *args, **kwds): # Assure type is erased on instantiation, # but attempt to store it in __orig_class__ @@ -1259,10 +1263,6 @@ def __new__(cls, *args, **kwds): return _generic_new(cls.__next_in_mro__, cls, *args, **kwds) -# prevent class and instance checks against plain Generic -Generic.__subclasshook__ = _make_subclasshook(Generic) - - class _TypingEmpty(object): """Internal placeholder for () or []. Used by TupleMeta and CallableMeta to allow empty list/tuple in specific places, without allowing them From 7156cf7a7e56d4146888db2c16a59f999d56b621 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 10 Feb 2017 20:27:35 +0100 Subject: [PATCH 5/5] Re-run Travis