From 443b24626c4842931896301b05b45a1cef4edbcf Mon Sep 17 00:00:00 2001 From: Galden Date: Mon, 20 Apr 2020 10:17:37 +0800 Subject: [PATCH 1/2] bpo-40336: Refactor typing._SpecialForm --- Lib/test/test_typing.py | 13 +++- Lib/typing.py | 140 +++++++++++++++++----------------------- 2 files changed, 69 insertions(+), 84 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 489836c459b1c8..d2e217608858c2 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -98,6 +98,7 @@ class A(type(Any)): pass def test_cannot_instantiate(self): + self.assertFalse(callable(Any)) with self.assertRaises(TypeError): Any() with self.assertRaises(TypeError): @@ -138,6 +139,7 @@ class A(type(NoReturn)): pass def test_cannot_instantiate(self): + self.assertFalse(callable(NoReturn)) with self.assertRaises(TypeError): NoReturn() with self.assertRaises(TypeError): @@ -212,6 +214,7 @@ class V(TypeVar): pass def test_cannot_instantiate_vars(self): + self.assertFalse(callable(TypeVar('A'))) with self.assertRaises(TypeError): TypeVar('A')() @@ -306,6 +309,7 @@ class C(Union[int, str]): pass def test_cannot_instantiate(self): + self.assertFalse(callable(Union)) with self.assertRaises(TypeError): Union() with self.assertRaises(TypeError): @@ -521,7 +525,8 @@ def test_repr(self): self.assertEqual(repr(Literal), "typing.Literal") self.assertEqual(repr(Literal[None]), "typing.Literal[None]") - def test_cannot_init(self): + def test_cannot_instantiate(self): + self.assertFalse(callable(Literal)) with self.assertRaises(TypeError): Literal() with self.assertRaises(TypeError): @@ -2228,7 +2233,8 @@ class C(type(ClassVar)): class C(type(ClassVar[int])): pass - def test_cannot_init(self): + def test_cannot_instantiate(self): + self.assertFalse(callable(ClassVar)) with self.assertRaises(TypeError): ClassVar() with self.assertRaises(TypeError): @@ -2271,7 +2277,8 @@ class C(type(Final)): class C(type(Final[int])): pass - def test_cannot_init(self): + def test_cannot_instantiate(self): + self.assertFalse(callable(Final)) with self.assertRaises(TypeError): Final() with self.assertRaises(TypeError): diff --git a/Lib/typing.py b/Lib/typing.py index df3650001e78ed..8f9051a1d8877c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -141,8 +141,9 @@ def _type_check(arg, msg, is_argument=True): if (isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms): raise TypeError(f"{arg} is not valid as type argument") - if (isinstance(arg, _SpecialForm) and arg not in (Any, NoReturn) or - arg in (Generic, Protocol)): + if arg in (Any, NoReturn): + return arg + if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol): raise TypeError(f"Plain {arg} is not valid as type argument") if isinstance(arg, (type, TypeVar, ForwardRef)): return arg @@ -299,41 +300,15 @@ def __deepcopy__(self, memo): return self -class _SpecialForm(_Final, _Immutable, _root=True): - """Internal indicator of special typing constructs. - See _doc instance attribute for specific docs. - """ - - __slots__ = ('_name', '_doc') - - def __new__(cls, *args, **kwds): - """Constructor. - - This only exists to give a better error message in case - someone tries to subclass a special typing object (not a good idea). - """ - if (len(args) == 3 and - isinstance(args[0], str) and - isinstance(args[1], tuple)): - # Close enough. - raise TypeError(f"Cannot subclass {cls!r}") - return super().__new__(cls) - - def __init__(self, name, doc): - self._name = name - self._doc = doc - - @property - def __doc__(self): - return self._doc - - def __eq__(self, other): - if not isinstance(other, _SpecialForm): - return NotImplemented - return self._name == other._name +# Internal indicator of special typing constructs. +# See __doc__ instance attribute for specific docs. +class _SpecialForm(_Final, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') - def __hash__(self): - return hash((self._name,)) + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ def __repr__(self): return 'typing.' + self._name @@ -341,9 +316,6 @@ def __repr__(self): def __reduce__(self): return self._name - def __call__(self, *args, **kwds): - raise TypeError(f"Cannot instantiate {self!r}") - def __instancecheck__(self, obj): raise TypeError(f"{self} cannot be used with isinstance()") @@ -352,31 +324,10 @@ def __subclasscheck__(self, cls): @_tp_cache def __getitem__(self, parameters): - if self._name in ('ClassVar', 'Final'): - item = _type_check(parameters, f'{self._name} accepts only single type.') - return _GenericAlias(self, (item,)) - if self._name == 'Union': - if parameters == (): - raise TypeError("Cannot take a Union of no types.") - if not isinstance(parameters, tuple): - parameters = (parameters,) - msg = "Union[arg, ...]: each arg must be a type." - parameters = tuple(_type_check(p, msg) for p in parameters) - parameters = _remove_dups_flatten(parameters) - if len(parameters) == 1: - return parameters[0] - return _GenericAlias(self, parameters) - if self._name == 'Optional': - arg = _type_check(parameters, "Optional[t] requires a single type.") - return Union[arg, type(None)] - if self._name == 'Literal': - # There is no '_type_check' call because arguments to Literal[...] are - # values, not types. - return _GenericAlias(self, parameters) - raise TypeError(f"{self} is not subscriptable") - - -Any = _SpecialForm('Any', doc= + return self._getitem(self, parameters) + +@_SpecialForm +def Any(self, parameters): """Special type indicating an unconstrained type. - Any is compatible with every type. @@ -386,9 +337,11 @@ def __getitem__(self, parameters): Note that all the above statements are true from the point of view of static type checkers. At runtime, Any should not be used with instance or class checks. - """) + """ + raise TypeError(f"{self} is not subscriptable") -NoReturn = _SpecialForm('NoReturn', doc= +@_SpecialForm +def NoReturn(self, parameters): """Special type indicating functions that never return. Example:: @@ -399,9 +352,11 @@ def stop() -> NoReturn: This type is invalid in other positions, e.g., ``List[NoReturn]`` will fail in static type checkers. - """) + """ + raise TypeError(f"{self} is not subscriptable") -ClassVar = _SpecialForm('ClassVar', doc= +@_SpecialForm +def ClassVar(self, parameters): """Special type construct to mark class variables. An annotation wrapped in ClassVar indicates that a given @@ -416,9 +371,12 @@ class Starship: Note that ClassVar is not a class itself, and should not be used with isinstance() or issubclass(). - """) + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _GenericAlias(self, (item,)) -Final = _SpecialForm('Final', doc= +@_SpecialForm +def Final(self, parameters): """Special typing construct to indicate final names to type checkers. A final name cannot be re-assigned or overridden in a subclass. @@ -434,9 +392,12 @@ class FastConnector(Connection): TIMEOUT = 1 # Error reported by type checker There is no runtime checking of these properties. - """) + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _GenericAlias(self, (item,)) -Union = _SpecialForm('Union', doc= +@_SpecialForm +def Union(self, parameters): """Union type; Union[X, Y] means either X or Y. To define a union, use e.g. Union[int, str]. Details: @@ -461,15 +422,29 @@ class FastConnector(Connection): - You cannot subclass or instantiate a union. - You can use Optional[X] as a shorthand for Union[X, None]. - """) - -Optional = _SpecialForm('Optional', doc= + """ + if parameters == (): + raise TypeError("Cannot take a Union of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + msg = "Union[arg, ...]: each arg must be a type." + parameters = tuple(_type_check(p, msg) for p in parameters) + parameters = _remove_dups_flatten(parameters) + if len(parameters) == 1: + return parameters[0] + return _GenericAlias(self, parameters) + +@_SpecialForm +def Optional(self, parameters): """Optional type. Optional[X] is equivalent to Union[X, None]. - """) + """ + arg = _type_check(parameters, f"{self} requires a single type.") + return Union[arg, type(None)] -Literal = _SpecialForm('Literal', doc= +@_SpecialForm +def Literal(self, parameters): """Special typing form to define literal types (a.k.a. value types). This form can be used to indicate to type checkers that the corresponding @@ -486,10 +461,13 @@ def open_helper(file: str, mode: MODE) -> str: open_helper('/some/path', 'r') # Passes type check open_helper('/other/path', 'typo') # Error in type checker - Literal[...] cannot be subclassed. At runtime, an arbitrary value - is allowed as type argument to Literal[...], but type checkers may - impose restrictions. - """) + Literal[...] cannot be subclassed. At runtime, an arbitrary value + is allowed as type argument to Literal[...], but type checkers may + impose restrictions. + """ + # There is no '_type_check' call because arguments to Literal[...] are + # values, not types. + return _GenericAlias(self, parameters) class ForwardRef(_Final, _root=True): From 4596cf207f584d38455cf9940f0a3c395e43b115 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Apr 2020 20:41:57 +0300 Subject: [PATCH 2/2] Restore __call__ and add __mro_entries__ for better error messages. --- Lib/test/test_typing.py | 13 +++---------- Lib/typing.py | 6 ++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d2e217608858c2..489836c459b1c8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -98,7 +98,6 @@ class A(type(Any)): pass def test_cannot_instantiate(self): - self.assertFalse(callable(Any)) with self.assertRaises(TypeError): Any() with self.assertRaises(TypeError): @@ -139,7 +138,6 @@ class A(type(NoReturn)): pass def test_cannot_instantiate(self): - self.assertFalse(callable(NoReturn)) with self.assertRaises(TypeError): NoReturn() with self.assertRaises(TypeError): @@ -214,7 +212,6 @@ class V(TypeVar): pass def test_cannot_instantiate_vars(self): - self.assertFalse(callable(TypeVar('A'))) with self.assertRaises(TypeError): TypeVar('A')() @@ -309,7 +306,6 @@ class C(Union[int, str]): pass def test_cannot_instantiate(self): - self.assertFalse(callable(Union)) with self.assertRaises(TypeError): Union() with self.assertRaises(TypeError): @@ -525,8 +521,7 @@ def test_repr(self): self.assertEqual(repr(Literal), "typing.Literal") self.assertEqual(repr(Literal[None]), "typing.Literal[None]") - def test_cannot_instantiate(self): - self.assertFalse(callable(Literal)) + def test_cannot_init(self): with self.assertRaises(TypeError): Literal() with self.assertRaises(TypeError): @@ -2233,8 +2228,7 @@ class C(type(ClassVar)): class C(type(ClassVar[int])): pass - def test_cannot_instantiate(self): - self.assertFalse(callable(ClassVar)) + def test_cannot_init(self): with self.assertRaises(TypeError): ClassVar() with self.assertRaises(TypeError): @@ -2277,8 +2271,7 @@ class C(type(Final)): class C(type(Final[int])): pass - def test_cannot_instantiate(self): - self.assertFalse(callable(Final)) + def test_cannot_init(self): with self.assertRaises(TypeError): Final() with self.assertRaises(TypeError): diff --git a/Lib/typing.py b/Lib/typing.py index 8f9051a1d8877c..c34f6ff69bf902 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -310,12 +310,18 @@ def __init__(self, getitem): self._name = getitem.__name__ self.__doc__ = getitem.__doc__ + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass {self!r}") + def __repr__(self): return 'typing.' + self._name def __reduce__(self): return self._name + def __call__(self, *args, **kwds): + raise TypeError(f"Cannot instantiate {self!r}") + def __instancecheck__(self, obj): raise TypeError(f"{self} cannot be used with isinstance()")