From 219b4ee5f98f5293e75024103f2f9d8400f577e2 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:04:32 +0800 Subject: [PATCH 01/24] Add ParamSpec and Concatenate --- Lib/typing.py | 166 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 32 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 46c54c406992f7..21ef6f370cde7d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -4,8 +4,8 @@ At large scale, the structure of the module is following: * Imports and exports, all public names should be explicitly added to __all__. * Internal helper functions: these should never be used in code outside this module. -* _SpecialForm and its instances (special forms): Any, NoReturn, ClassVar, Union, Optional -* Two classes whose instances can be type arguments in addition to types: ForwardRef and TypeVar +* _SpecialForm and its instances (special forms): Any, NoReturn, ClassVar, Union, Optional, Concatenate +* Three classes whose instances can be type arguments in addition to types: ForwardRef, TypeVar and ParamSpec * The core of internal generics API: _GenericAlias and _VariadicGenericAlias, the latter is currently only used by Tuple and Callable. All subscripted types like X[int], Union[int, str], etc., are instances of either of these classes. @@ -36,11 +36,13 @@ 'Any', 'Callable', 'ClassVar', + 'Concatenate', 'Final', 'ForwardRef', 'Generic', 'Literal', 'Optional', + 'ParamSpec', 'Protocol', 'Tuple', 'Type', @@ -147,7 +149,7 @@ def _type_check(arg, msg, is_argument=True): 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, types.Union)): + if isinstance(arg, (type, TypeVar, ForwardRef, types.Union, ParamSpec)): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") @@ -516,6 +518,22 @@ def TypeAlias(self, parameters): raise TypeError(f"{self} is not subscriptable") +@_SpecialForm +def Concatenate(self, parameters): + """Used in conjunction with ParamSpec and Callable to represent a higher + order function which add, removes or transform parameters of a Callable. + """ + if parameters == (): + raise TypeError("Cannot take a Concatenate of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = tuple(_type_check(p, msg) for p in parameters) + if not any(isinstance(p, ParamSpec) for p in parameters): + raise TypeError("Concatenate must contain at least one ParamSpec variable.") + return _ConcatenateGenericAlias(self, parameters) + + class ForwardRef(_Final, _root=True): """Internal wrapper to hold a forward reference.""" @@ -578,8 +596,41 @@ def __hash__(self): def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' +class _TypeVarLike: + """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" + def _setup_bound_cov_contra(self, bound, covariant, contravariant): + """Used to setup TypeVars and ParamSpec's bound, covariant and + contravariant attributes. + """ + if covariant and contravariant: + raise ValueError("Bivariant types are not supported.") + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + if bound: + self.__bound__ = _type_check(bound, "Bound must be a type.") + else: + self.__bound__ = None -class TypeVar(_Final, _Immutable, _root=True): + def __or__(self, right): + return Union[self, right] + + def __ror__(self, right): + return Union[self, right] + + def __repr__(self): + if self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ + + def __reduce__(self): + return self.__name__ + + +class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): """Type variable. Usage:: @@ -629,20 +680,14 @@ def longest(x: A, y: A) -> A: def __init__(self, name, *constraints, bound=None, covariant=False, contravariant=False): self.__name__ = name - if covariant and contravariant: - raise ValueError("Bivariant types are not supported.") - self.__covariant__ = bool(covariant) - self.__contravariant__ = bool(contravariant) + self._setup_bound_cov_contra(bound=bound, covariant=covariant, + contravariant=contravariant) if constraints and bound is not None: raise TypeError("Constraints cannot be combined with bound=...") if constraints and len(constraints) == 1: raise TypeError("A single constraint is not allowed") msg = "TypeVar(name, constraint, ...): constraints must be types." self.__constraints__ = tuple(_type_check(t, msg) for t in constraints) - if bound: - self.__bound__ = _type_check(bound, "Bound must be a type.") - else: - self.__bound__ = None try: def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') # for pickling except (AttributeError, ValueError): @@ -650,23 +695,69 @@ def __init__(self, name, *constraints, bound=None, if def_mod != 'typing': self.__module__ = def_mod - def __or__(self, right): - return Union[self, right] - def __ror__(self, right): - return Union[self, right] +class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): + """Parameter specification variable. - def __repr__(self): - if self.__covariant__: - prefix = '+' - elif self.__contravariant__: - prefix = '-' - else: - prefix = '~' - return prefix + self.__name__ + Usage:: - def __reduce__(self): - return self.__name__ + P = ParamSpec('P') + + Parameter specification variables exist primarily for the benefit of static + type checkers. They serve primarily to forward the parameter types of one + Callable to another Callable, a pattern commonly found in higher order + functions and decorators. They are only valid as the first argument to + Callable, or as parameters for user-defined Generics. See class Generic + for more information on generic types. An example for annotating a + decorator:: + + T = TypeVar('T') + P = ParamSpec('P') + + def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''A decorator to add logging to a function.''' + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + logging.info(f'{f.__name__} was called') + return f(*args, **kwargs) + return inner + + @add_logging + def add_two(x: float, y: float) -> float: + '''Add two numbers together.''' + return x + y + + Parameter specification variables defined with covariant=True or + contravariant=True can be used to declare covariant or contravariant + generic types. These keyword arguments are valid, but their actual semantics + are yet to be decided. See PEP 612 for details. + + Parameter specification variables can be introspected. e.g.: + + P.__name__ == 'T' + P.__bound__ == None + P.__covariant__ == False + P.__contravariant__ == False + + Note that only parameter specification variables defined in global scope can + be pickled. + """ + + __slots__ = ('__name__', '__bound__', '__covariant__', '__contravariant__', + '__dict__') + + args = object() + kwargs = object() + + def __init__(self, name, bound=None, covariant=False, contravariant=False): + self.__name__ = name + self._setup_bound_cov_contra(bound=bound, covariant=covariant, + contravariant=contravariant) + try: + def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + def_mod = None + if def_mod != 'typing': + self.__module__ = def_mod def _is_dunder(attr): @@ -874,18 +965,22 @@ def __or__(self, right): def __ror__(self, right): return Union[self, right] + class _CallableGenericAlias(_GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' - if len(self.__args__) == 2 and self.__args__[0] is Ellipsis: + args = self.__args__ + if len(args) == 2 and (args[0] is Ellipsis + or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias))): return super().__repr__() return (f'typing.Callable' - f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' - f'{_type_repr(self.__args__[-1])}]') + f'[[{", ".join([_type_repr(a) for a in args[:-1]])}], ' + f'{_type_repr(args[-1])}]') def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and args[0] is ...): + if not (len(args) == 2 and (args[0] is Ellipsis + or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias)))): args = list(args[:-1]), args[-1] return operator.getitem, (Callable, args) @@ -903,10 +998,11 @@ def __getitem__(self, params): if args is Ellipsis: params = (Ellipsis, result) else: - if not isinstance(args, list): + if not isinstance(args, (list, ParamSpec, _ConcatenateGenericAlias)): raise TypeError(f"Callable[args, result]: args must be a list." f" Got {args}") - params = (tuple(args), result) + if isinstance(args, list): + params = (tuple(args), result) return self.__getitem_inner__(params) @_tp_cache @@ -916,6 +1012,8 @@ def __getitem_inner__(self, params): result = _type_check(result, msg) if args is Ellipsis: return self.copy_with((_TypingEllipsis, result)) + elif isinstance(args, (ParamSpec, _ConcatenateGenericAlias)): + return self.copy_with((args, result)) msg = "Callable[[arg, ...], result]: each arg must be a type." args = tuple(_type_check(arg, msg) for arg in args) params = args + (result,) @@ -984,6 +1082,10 @@ def __hash__(self): return hash(frozenset(_value_and_type_iter(self.__args__))) +class _ConcatenateGenericAlias(_GenericAlias, _root=True): + pass + + class Generic: """Abstract base class for generic types. From a1c0d0ae1f12d3adaeb9a4209b030d959b576e26 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:18:17 +0800 Subject: [PATCH 02/24] support ParamSpec in generics --- Lib/typing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 21ef6f370cde7d..1cdd0c445582c7 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1120,9 +1120,10 @@ def __class_getitem__(cls, params): params = tuple(_type_check(p, msg) for p in params) if cls in (Generic, Protocol): # Generic and Protocol can only be subscripted with unique type variables. - if not all(isinstance(p, TypeVar) for p in params): + if not all(isinstance(p, (TypeVar, ParamSpec)) for p in params): raise TypeError( - f"Parameters to {cls.__name__}[...] must all be type variables") + f"Parameters to {cls.__name__}[...] must all be type variables " + f"or parameter specification variables.") if len(set(params)) != len(params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be unique") From 7b3beabd466d648bbda0fb386c506f91114a553f Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 10 Dec 2020 00:07:40 +0800 Subject: [PATCH 03/24] Add typing tests, disallow Concatenate in other types --- Lib/test/test_typing.py | 185 +++++++++++++++++++++++++++++++++++----- Lib/typing.py | 11 ++- 2 files changed, 174 insertions(+), 22 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8ffc7f40cebdd2..e99525d8416b8f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -25,6 +25,7 @@ from typing import Pattern, Match from typing import Annotated, ForwardRef from typing import TypeAlias +from typing import ParamSpec, Concatenate import abc import typing import weakref @@ -1573,7 +1574,8 @@ class C(B[int]): def test_subscripted_generics_as_proxies(self): T = TypeVar('T') - class C(Generic[T]): + P = ParamSpec('T') + class C(Generic[T, P]): x = 'def' self.assertEqual(C[int].x, 'def') self.assertEqual(C[C[int]].x, 'def') @@ -1705,11 +1707,12 @@ class Meta(type): ... self.assertEqual(Callable[..., Meta].__args__, (Ellipsis, Meta)) def test_generic_hashes(self): - class A(Generic[T]): + P = ParamSpec('P') + class A(Generic[T, P]): ... - class B(Generic[T]): - class A(Generic[T]): + class B(Generic[T, P]): + class A(Generic[T, P]): ... self.assertEqual(A, A) @@ -1892,8 +1895,10 @@ def test_all_repr_eq_any(self): def test_pickle(self): global C # pickle wants to reference the class by name T = TypeVar('T') + global P + P = ParamSpec('P') - class B(Generic[T]): + class B(Generic[T, P]): pass class C(B[int]): @@ -1917,7 +1922,8 @@ class C(B[int]): x = pickle.loads(z) self.assertEqual(s, x) more_samples = [List, typing.Iterable, typing.Type, List[int], - typing.Type[typing.Mapping], typing.AbstractSet[Tuple[int, str]]] + typing.Type[typing.Mapping], typing.AbstractSet[Tuple[int, str]], + Concatenate[int, P]] for s in more_samples: for proto in range(pickle.HIGHEST_PROTOCOL + 1): z = pickle.dumps(s, proto) @@ -1926,7 +1932,8 @@ class C(B[int]): def test_copy_and_deepcopy(self): T = TypeVar('T') - class Node(Generic[T]): ... + P = ParamSpec('P') + class Node(Generic[T, P]): ... things = [Union[T, int], Tuple[T, int], Callable[..., T], Callable[[int], int], Tuple[Any, Any], Node[T], Node[int], Node[Any], typing.Iterable[T], typing.Iterable[Any], typing.Iterable[int], typing.Dict[int, str], @@ -1958,7 +1965,10 @@ def test_immutability_by_copy_and_pickle(self): def test_copy_generic_instances(self): T = TypeVar('T') - class C(Generic[T]): + P = ParamSpec('P') + + class C(Generic[T, P]): + f: Callable[P, int] def __init__(self, attr: T) -> None: self.attr = attr @@ -1990,7 +2000,9 @@ def test_weakref_all(self): def test_parameterized_slots(self): T = TypeVar('T') - class C(Generic[T]): + P = ParamSpec('P') + + class C(Generic[T, P]): __slots__ = ('potato',) c = C() @@ -2009,7 +2021,9 @@ def foo(x: C['C']): ... def test_parameterized_slots_dict(self): T = TypeVar('T') - class D(Generic[T]): + P = ParamSpec('P') + + class D(Generic[T, P]): __slots__ = {'banana': 42} d = D() @@ -2030,7 +2044,9 @@ class C(Generic[B]): pass def test_repr_2(self): - class C(Generic[T]): + P = ParamSpec('P') + + class C(Generic[T, P]): pass self.assertEqual(C.__module__, __name__) @@ -2066,8 +2082,9 @@ class B(Generic[T]): self.assertNotEqual(A[T], B[T]) def test_multiple_inheritance(self): + P = ParamSpec('P') - class A(Generic[T, VT]): + class A(Generic[T, VT, P]): pass class B(Generic[KT, T]): @@ -2080,7 +2097,9 @@ class C(A[T, VT], Generic[VT, T, KT], B[KT, T]): def test_multiple_inheritance_special(self): S = TypeVar('S') - class B(Generic[S]): ... + P = ParamSpec('P') + + class B(Generic[S, P]): ... class C(List[int], B): ... self.assertEqual(C.__mro__, (C, list, B, Generic, object)) @@ -2094,7 +2113,8 @@ def __init_subclass__(cls, **kwargs) -> None: if base is not Final and issubclass(base, Final): raise FinalException(base) super().__init_subclass__(**kwargs) - class Test(Generic[T], Final): + P = ParamSpec('P') + class Test(Generic[T, P], Final): pass with self.assertRaises(FinalException): class Subclass(Test): @@ -2104,10 +2124,10 @@ class Subclass(Test[int]): pass def test_nested(self): - + P = ParamSpec('P') G = Generic - class Visitor(G[T]): + class Visitor(G[T, P]): a = None @@ -2135,8 +2155,9 @@ def append(self, x: int): def test_type_erasure(self): T = TypeVar('T') + P = ParamSpec('P') - class Node(Generic[T]): + class Node(Generic[T, P]): def __init__(self, label: T, left: 'Node[T]' = None, right: 'Node[T]' = None): @@ -2159,8 +2180,9 @@ def foo(x: T): def test_implicit_any(self): T = TypeVar('T') + P = ParamSpec('P') - class C(Generic[T]): + class C(Generic[T, P]): pass class D(C): @@ -2176,8 +2198,9 @@ class D(C): D[T] def test_new_with_args(self): + P = ParamSpec('P') - class A(Generic[T]): + class A(Generic[T, P]): pass class B: @@ -2214,8 +2237,9 @@ def __init__(self, arg): self.assertEqual(c.from_c, 'foo') def test_new_no_args(self): + P = ParamSpec('P') - class A(Generic[T]): + class A(Generic[T, P]): pass with self.assertRaises(TypeError): @@ -4253,6 +4277,127 @@ def test_cannot_subscript(self): TypeAlias[int] +class ParamSpecTests(BaseTestCase): + + def test_basic_plain(self): + P = ParamSpec('P') + self.assertEqual(P, P) + self.assertIsInstance(P, ParamSpec) + + def test_valid_uses(self): + P = ParamSpec('P') + T = TypeVar('T') + C1 = Callable[P, int] + self.assertEqual(C1.__args__, (P, int)) + self.assertEqual(C1.__parameters__, ()) + C2 = Callable[P, T] + self.assertEqual(C2.__args__, (P, T)) + self.assertEqual(C2.__parameters__, (T,)) + # Test collections.abc.Callable too. + C3 = collections.abc.Callable[P, int] + self.assertEqual(C3.__args__, (P, int)) + self.assertEqual(C3.__parameters__, ()) + C4 = collections.abc.Callable[P, T] + self.assertEqual(C4.__args__, (P, T)) + self.assertEqual(C4.__parameters__, (T,)) + + # ParamSpec instances should also have args and kwargs attributes. + self.assertIn('args', dir(P)) + self.assertIn('kwargs', dir(P)) + P.args + P.kwargs + + def test_instance_type_error(self): + P = ParamSpec('P') + with self.assertRaises(TypeError): + isinstance(42, P) + + def test_instance_type_error(self): + P = ParamSpec('P') + with self.assertRaises(TypeError): + issubclass(int, P) + with self.assertRaises(TypeError): + issubclass(P, int) + + def test_union_unique(self): + P1 = ParamSpec('P1') + P2 = ParamSpec('P2') + self.assertNotEqual(P1, P2) + self.assertEqual(Union[P1], P1) + self.assertNotEqual(Union[P1], Union[P1, P2]) + self.assertEqual(Union[P1, P1], P1) + self.assertNotEqual(Union[P1, int], Union[P1]) + self.assertNotEqual(Union[P1, int], Union[int]) + self.assertEqual(Union[P1, int].__args__, (P1, int)) + self.assertEqual(Union[P1, int].__parameters__, ()) + self.assertIs(Union[P1, int].__origin__, Union) + + def test_repr(self): + P = ParamSpec('P') + self.assertEqual(repr(P), '~P') + P_co = ParamSpec('P_co', covariant=True) + self.assertEqual(repr(P_co), '+P_co') + P_contra = ParamSpec('P_contra', contravariant=True) + self.assertEqual(repr(P_contra), '-P_contra') + + def test_no_redefinition(self): + self.assertNotEqual(ParamSpec('P'), ParamSpec('P')) + self.assertNotEqual(ParamSpec('P', int, str), ParamSpec('P', int, str)) + + def test_cannot_subclass_vars(self): + with self.assertRaises(TypeError): + class V(ParamSpec('P')): + pass + + def test_cannot_subclass_var_itself(self): + with self.assertRaises(TypeError): + class V(ParamSpec): + pass + + def test_cannot_instantiate_vars(self): + with self.assertRaises(TypeError): + ParamSpec('A')() + + def test_no_bivariant(self): + with self.assertRaises(ValueError): + ParamSpec('P', covariant=True, contravariant=True) + + +class ConcatenateTests(BaseTestCase): + def test_basics(self): + P = ParamSpec('P') + class MyClass: ... + c = Concatenate[MyClass, P] + self.assertNotEqual(c, Concatenate) + + def test_valid_uses(self): + P = ParamSpec('P') + T = TypeVar('T') + C1 = Callable[Concatenate[int, P], int] + self.assertEqual(C1.__args__, (Concatenate[int, P], int)) + self.assertEqual(C1.__parameters__, ()) + C2 = Callable[Concatenate[int, P, T], T] + self.assertEqual(C2.__args__, (Concatenate[int, P, T], T)) + self.assertEqual(C2.__parameters__, (T,)) + + # Test collections.abc.Callable too. + C3 = collections.abc.Callable[Concatenate[int, P], int] + self.assertEqual(C3.__args__, (Concatenate[int, P], int)) + self.assertEqual(C3.__parameters__, ()) + C4 = collections.abc.Callable[Concatenate[int, P, T], T] + self.assertEqual(C4.__args__, (Concatenate[int, P, T], T)) + self.assertEqual(C4.__parameters__, (T,)) + + def test_disallow_in_other_types(self): + P = ParamSpec('P') + C = Concatenate[int, P] + samples = [Any, Union, Tuple, ClassVar, List, + typing.DefaultDict, typing.FrozenSet] + for s in samples: + with self.subTest(f'{s}'): + with self.assertRaises(TypeError): + s[C] + class AllTests(BaseTestCase): """Tests for __all__.""" diff --git a/Lib/typing.py b/Lib/typing.py index 1cdd0c445582c7..4af91ae326f71a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -153,6 +153,9 @@ def _type_check(arg, msg, is_argument=True): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") + if isinstance(arg, _ConcatenateGenericAlias): + raise TypeError(f"{arg} is not valid as a type argument " + "except in Callable.") return arg @@ -965,7 +968,6 @@ def __or__(self, right): def __ror__(self, right): return Union[self, right] - class _CallableGenericAlias(_GenericAlias, _root=True): def __repr__(self): assert self._name == 'Callable' @@ -1083,7 +1085,12 @@ def __hash__(self): class _ConcatenateGenericAlias(_GenericAlias, _root=True): - pass + + def __or__(self, right): + return NotImplemented + + def __ror__(self, right): + return NotImplemented class Generic: From 5dd3b4480baffa25c993a78d85c8e78bb366a50b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 10 Dec 2020 00:09:48 +0800 Subject: [PATCH 04/24] Add news --- .../next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst diff --git a/Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst b/Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst new file mode 100644 index 00000000000000..539fdeccd14b3c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst @@ -0,0 +1,2 @@ +Implemented :pep:`612`: added ``ParamSpec`` and ``Concatenate`` to +:mod:`typing`. Patch by Ken Jin. From 59c0b20eb2c6158807a03b17d8e6677c3c73e02b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 10 Dec 2020 22:34:35 +0800 Subject: [PATCH 05/24] Address some of Guido's review comments Co-Authored-By: Guido van Rossum --- Lib/test/test_typing.py | 88 ++++++++++++++++++----------------------- Lib/typing.py | 37 ++++++++++------- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e99525d8416b8f..3397242e939066 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1574,8 +1574,8 @@ class C(B[int]): def test_subscripted_generics_as_proxies(self): T = TypeVar('T') - P = ParamSpec('T') - class C(Generic[T, P]): + + class C(Generic[T]): x = 'def' self.assertEqual(C[int].x, 'def') self.assertEqual(C[C[int]].x, 'def') @@ -1707,12 +1707,12 @@ class Meta(type): ... self.assertEqual(Callable[..., Meta].__args__, (Ellipsis, Meta)) def test_generic_hashes(self): - P = ParamSpec('P') - class A(Generic[T, P]): + + class A(Generic[T]): ... - class B(Generic[T, P]): - class A(Generic[T, P]): + class B(Generic[T]): + class A(Generic[T]): ... self.assertEqual(A, A) @@ -1896,9 +1896,9 @@ def test_pickle(self): global C # pickle wants to reference the class by name T = TypeVar('T') global P - P = ParamSpec('P') + - class B(Generic[T, P]): + class B(Generic[T]): pass class C(B[int]): @@ -1922,8 +1922,7 @@ class C(B[int]): x = pickle.loads(z) self.assertEqual(s, x) more_samples = [List, typing.Iterable, typing.Type, List[int], - typing.Type[typing.Mapping], typing.AbstractSet[Tuple[int, str]], - Concatenate[int, P]] + typing.Type[typing.Mapping], typing.AbstractSet[Tuple[int, str]]] for s in more_samples: for proto in range(pickle.HIGHEST_PROTOCOL + 1): z = pickle.dumps(s, proto) @@ -1932,8 +1931,8 @@ class C(B[int]): def test_copy_and_deepcopy(self): T = TypeVar('T') - P = ParamSpec('P') - class Node(Generic[T, P]): ... + + class Node(Generic[T]): ... things = [Union[T, int], Tuple[T, int], Callable[..., T], Callable[[int], int], Tuple[Any, Any], Node[T], Node[int], Node[Any], typing.Iterable[T], typing.Iterable[Any], typing.Iterable[int], typing.Dict[int, str], @@ -1965,9 +1964,9 @@ def test_immutability_by_copy_and_pickle(self): def test_copy_generic_instances(self): T = TypeVar('T') - P = ParamSpec('P') + - class C(Generic[T, P]): + class C(Generic[T]): f: Callable[P, int] def __init__(self, attr: T) -> None: self.attr = attr @@ -2000,9 +1999,9 @@ def test_weakref_all(self): def test_parameterized_slots(self): T = TypeVar('T') - P = ParamSpec('P') + - class C(Generic[T, P]): + class C(Generic[T]): __slots__ = ('potato',) c = C() @@ -2021,9 +2020,9 @@ def foo(x: C['C']): ... def test_parameterized_slots_dict(self): T = TypeVar('T') - P = ParamSpec('P') + - class D(Generic[T, P]): + class D(Generic[T]): __slots__ = {'banana': 42} d = D() @@ -2044,9 +2043,9 @@ class C(Generic[B]): pass def test_repr_2(self): - P = ParamSpec('P') + - class C(Generic[T, P]): + class C(Generic[T]): pass self.assertEqual(C.__module__, __name__) @@ -2082,9 +2081,9 @@ class B(Generic[T]): self.assertNotEqual(A[T], B[T]) def test_multiple_inheritance(self): - P = ParamSpec('P') + - class A(Generic[T, VT, P]): + class A(Generic[T, VT]): pass class B(Generic[KT, T]): @@ -2097,9 +2096,9 @@ class C(A[T, VT], Generic[VT, T, KT], B[KT, T]): def test_multiple_inheritance_special(self): S = TypeVar('S') - P = ParamSpec('P') + - class B(Generic[S, P]): ... + class B(Generic[S]): ... class C(List[int], B): ... self.assertEqual(C.__mro__, (C, list, B, Generic, object)) @@ -2113,8 +2112,8 @@ def __init_subclass__(cls, **kwargs) -> None: if base is not Final and issubclass(base, Final): raise FinalException(base) super().__init_subclass__(**kwargs) - P = ParamSpec('P') - class Test(Generic[T, P], Final): + + class Test(Generic[T], Final): pass with self.assertRaises(FinalException): class Subclass(Test): @@ -2124,10 +2123,10 @@ class Subclass(Test[int]): pass def test_nested(self): - P = ParamSpec('P') + G = Generic - class Visitor(G[T, P]): + class Visitor(G[T]): a = None @@ -2155,9 +2154,9 @@ def append(self, x: int): def test_type_erasure(self): T = TypeVar('T') - P = ParamSpec('P') + - class Node(Generic[T, P]): + class Node(Generic[T]): def __init__(self, label: T, left: 'Node[T]' = None, right: 'Node[T]' = None): @@ -2180,9 +2179,9 @@ def foo(x: T): def test_implicit_any(self): T = TypeVar('T') - P = ParamSpec('P') + - class C(Generic[T, P]): + class C(Generic[T]): pass class D(C): @@ -2198,9 +2197,9 @@ class D(C): D[T] def test_new_with_args(self): - P = ParamSpec('P') + - class A(Generic[T, P]): + class A(Generic[T]): pass class B: @@ -2237,9 +2236,9 @@ def __init__(self, arg): self.assertEqual(c.from_c, 'foo') def test_new_no_args(self): - P = ParamSpec('P') + - class A(Generic[T, P]): + class A(Generic[T]): pass with self.assertRaises(TypeError): @@ -4376,27 +4375,18 @@ def test_valid_uses(self): C1 = Callable[Concatenate[int, P], int] self.assertEqual(C1.__args__, (Concatenate[int, P], int)) self.assertEqual(C1.__parameters__, ()) - C2 = Callable[Concatenate[int, P, T], T] - self.assertEqual(C2.__args__, (Concatenate[int, P, T], T)) + C2 = Callable[Concatenate[int, T, P], T] + self.assertEqual(C2.__args__, (Concatenate[int, T, P], T)) self.assertEqual(C2.__parameters__, (T,)) # Test collections.abc.Callable too. C3 = collections.abc.Callable[Concatenate[int, P], int] self.assertEqual(C3.__args__, (Concatenate[int, P], int)) self.assertEqual(C3.__parameters__, ()) - C4 = collections.abc.Callable[Concatenate[int, P, T], T] - self.assertEqual(C4.__args__, (Concatenate[int, P, T], T)) + C4 = collections.abc.Callable[Concatenate[int, T, P], T] + self.assertEqual(C4.__args__, (Concatenate[int, T, P], T)) self.assertEqual(C4.__parameters__, (T,)) - def test_disallow_in_other_types(self): - P = ParamSpec('P') - C = Concatenate[int, P] - samples = [Any, Union, Tuple, ClassVar, List, - typing.DefaultDict, typing.FrozenSet] - for s in samples: - with self.subTest(f'{s}'): - with self.assertRaises(TypeError): - s[C] class AllTests(BaseTestCase): """Tests for __all__.""" diff --git a/Lib/typing.py b/Lib/typing.py index 4af91ae326f71a..6936a1052ca7a6 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -4,8 +4,10 @@ At large scale, the structure of the module is following: * Imports and exports, all public names should be explicitly added to __all__. * Internal helper functions: these should never be used in code outside this module. -* _SpecialForm and its instances (special forms): Any, NoReturn, ClassVar, Union, Optional, Concatenate -* Three classes whose instances can be type arguments in addition to types: ForwardRef, TypeVar and ParamSpec +* _SpecialForm and its instances (special forms): + Any, NoReturn, ClassVar, Union, Optional, Concatenate +* Classes whose instances can be type arguments in addition to types: + ForwardRef, TypeVar and ParamSpec * The core of internal generics API: _GenericAlias and _VariadicGenericAlias, the latter is currently only used by Tuple and Callable. All subscripted types like X[int], Union[int, str], etc., are instances of either of these classes. @@ -153,9 +155,6 @@ def _type_check(arg, msg, is_argument=True): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") - if isinstance(arg, _ConcatenateGenericAlias): - raise TypeError(f"{arg} is not valid as a type argument " - "except in Callable.") return arg @@ -524,7 +523,16 @@ def TypeAlias(self, parameters): @_SpecialForm def Concatenate(self, parameters): """Used in conjunction with ParamSpec and Callable to represent a higher - order function which add, removes or transform parameters of a Callable. + order function which adds, removes or transforms parameters of a Callable. + + For example:: + + Callable[Concatenate[int, P], int] + + .. seealso:: + + :pep:`612` -- Parameter Specification Variables + """ if parameters == (): raise TypeError("Cannot take a Concatenate of no types.") @@ -532,8 +540,9 @@ def Concatenate(self, parameters): parameters = (parameters,) msg = "Concatenate[arg, ...]: each arg must be a type." parameters = tuple(_type_check(p, msg) for p in parameters) - if not any(isinstance(p, ParamSpec) for p in parameters): - raise TypeError("Concatenate must contain at least one ParamSpec variable.") + if not isinstance(parameters[-1], ParamSpec): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable.") return _ConcatenateGenericAlias(self, parameters) @@ -601,7 +610,7 @@ def __repr__(self): class _TypeVarLike: """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" - def _setup_bound_cov_contra(self, bound, covariant, contravariant): + def __init__(self, bound, covariant, contravariant): """Used to setup TypeVars and ParamSpec's bound, covariant and contravariant attributes. """ @@ -683,8 +692,7 @@ def longest(x: A, y: A) -> A: def __init__(self, name, *constraints, bound=None, covariant=False, contravariant=False): self.__name__ = name - self._setup_bound_cov_contra(bound=bound, covariant=covariant, - contravariant=contravariant) + super().__init__(bound, covariant, contravariant) if constraints and bound is not None: raise TypeError("Constraints cannot be combined with bound=...") if constraints and len(constraints) == 1: @@ -707,7 +715,7 @@ class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): P = ParamSpec('P') Parameter specification variables exist primarily for the benefit of static - type checkers. They serve primarily to forward the parameter types of one + type checkers. They are used to forward the parameter types of one Callable to another Callable, a pattern commonly found in higher order functions and decorators. They are only valid as the first argument to Callable, or as parameters for user-defined Generics. See class Generic @@ -718,7 +726,7 @@ class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): P = ParamSpec('P') def add_logging(f: Callable[P, T]) -> Callable[P, T]: - '''A decorator to add logging to a function.''' + '''A type-safe decorator to add logging to a function.''' def inner(*args: P.args, **kwargs: P.kwargs) -> T: logging.info(f'{f.__name__} was called') return f(*args, **kwargs) @@ -753,8 +761,7 @@ def add_two(x: float, y: float) -> float: def __init__(self, name, bound=None, covariant=False, contravariant=False): self.__name__ = name - self._setup_bound_cov_contra(bound=bound, covariant=covariant, - contravariant=contravariant) + super().__init__(bound, covariant, contravariant) try: def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') except (AttributeError, ValueError): From 4c381b36ea12aaf5d221677f4fe4982b15c608f3 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 10 Dec 2020 22:39:29 +0800 Subject: [PATCH 06/24] remove extraneous empty lines --- Lib/test/test_typing.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3397242e939066..cf2bf1b4f78df4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1574,7 +1574,6 @@ class C(B[int]): def test_subscripted_generics_as_proxies(self): T = TypeVar('T') - class C(Generic[T]): x = 'def' self.assertEqual(C[int].x, 'def') @@ -1707,7 +1706,6 @@ class Meta(type): ... self.assertEqual(Callable[..., Meta].__args__, (Ellipsis, Meta)) def test_generic_hashes(self): - class A(Generic[T]): ... @@ -1895,8 +1893,6 @@ def test_all_repr_eq_any(self): def test_pickle(self): global C # pickle wants to reference the class by name T = TypeVar('T') - global P - class B(Generic[T]): pass @@ -1931,7 +1927,6 @@ class C(B[int]): def test_copy_and_deepcopy(self): T = TypeVar('T') - class Node(Generic[T]): ... things = [Union[T, int], Tuple[T, int], Callable[..., T], Callable[[int], int], Tuple[Any, Any], Node[T], Node[int], Node[Any], typing.Iterable[T], @@ -1964,10 +1959,7 @@ def test_immutability_by_copy_and_pickle(self): def test_copy_generic_instances(self): T = TypeVar('T') - - class C(Generic[T]): - f: Callable[P, int] def __init__(self, attr: T) -> None: self.attr = attr @@ -1999,8 +1991,6 @@ def test_weakref_all(self): def test_parameterized_slots(self): T = TypeVar('T') - - class C(Generic[T]): __slots__ = ('potato',) @@ -2020,8 +2010,6 @@ def foo(x: C['C']): ... def test_parameterized_slots_dict(self): T = TypeVar('T') - - class D(Generic[T]): __slots__ = {'banana': 42} @@ -2043,8 +2031,6 @@ class C(Generic[B]): pass def test_repr_2(self): - - class C(Generic[T]): pass @@ -2081,7 +2067,6 @@ class B(Generic[T]): self.assertNotEqual(A[T], B[T]) def test_multiple_inheritance(self): - class A(Generic[T, VT]): pass @@ -2096,8 +2081,6 @@ class C(A[T, VT], Generic[VT, T, KT], B[KT, T]): def test_multiple_inheritance_special(self): S = TypeVar('S') - - class B(Generic[S]): ... class C(List[int], B): ... self.assertEqual(C.__mro__, (C, list, B, Generic, object)) @@ -2112,7 +2095,6 @@ def __init_subclass__(cls, **kwargs) -> None: if base is not Final and issubclass(base, Final): raise FinalException(base) super().__init_subclass__(**kwargs) - class Test(Generic[T], Final): pass with self.assertRaises(FinalException): @@ -2123,7 +2105,7 @@ class Subclass(Test[int]): pass def test_nested(self): - + G = Generic class Visitor(G[T]): @@ -2154,7 +2136,6 @@ def append(self, x: int): def test_type_erasure(self): T = TypeVar('T') - class Node(Generic[T]): def __init__(self, label: T, @@ -2179,7 +2160,6 @@ def foo(x: T): def test_implicit_any(self): T = TypeVar('T') - class C(Generic[T]): pass @@ -2197,7 +2177,6 @@ class D(C): D[T] def test_new_with_args(self): - class A(Generic[T]): pass @@ -2236,7 +2215,6 @@ def __init__(self, arg): self.assertEqual(c.from_c, 'foo') def test_new_no_args(self): - class A(Generic[T]): pass From b36b62dc46e859b8afa836ca1a8ed15114c3f7a4 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 10 Dec 2020 23:14:48 +0800 Subject: [PATCH 07/24] Support ParamSpec in __parameters__ of typing and builtin GenericAlias also reduced runtime checks --- Lib/test/test_typing.py | 18 +++++++++--------- Lib/typing.py | 23 ++++++++--------------- Objects/genericaliasobject.c | 5 +++-- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index cf2bf1b4f78df4..1b1232b2604b41 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4266,17 +4266,17 @@ def test_valid_uses(self): T = TypeVar('T') C1 = Callable[P, int] self.assertEqual(C1.__args__, (P, int)) - self.assertEqual(C1.__parameters__, ()) + self.assertEqual(C1.__parameters__, (P,)) C2 = Callable[P, T] self.assertEqual(C2.__args__, (P, T)) - self.assertEqual(C2.__parameters__, (T,)) + self.assertEqual(C2.__parameters__, (P, T)) # Test collections.abc.Callable too. C3 = collections.abc.Callable[P, int] self.assertEqual(C3.__args__, (P, int)) - self.assertEqual(C3.__parameters__, ()) + self.assertEqual(C3.__parameters__, (P,)) C4 = collections.abc.Callable[P, T] self.assertEqual(C4.__args__, (P, T)) - self.assertEqual(C4.__parameters__, (T,)) + self.assertEqual(C4.__parameters__, (P, T)) # ParamSpec instances should also have args and kwargs attributes. self.assertIn('args', dir(P)) @@ -4306,7 +4306,7 @@ def test_union_unique(self): self.assertNotEqual(Union[P1, int], Union[P1]) self.assertNotEqual(Union[P1, int], Union[int]) self.assertEqual(Union[P1, int].__args__, (P1, int)) - self.assertEqual(Union[P1, int].__parameters__, ()) + self.assertEqual(Union[P1, int].__parameters__, (P1,)) self.assertIs(Union[P1, int].__origin__, Union) def test_repr(self): @@ -4352,18 +4352,18 @@ def test_valid_uses(self): T = TypeVar('T') C1 = Callable[Concatenate[int, P], int] self.assertEqual(C1.__args__, (Concatenate[int, P], int)) - self.assertEqual(C1.__parameters__, ()) + self.assertEqual(C1.__parameters__, (P,)) C2 = Callable[Concatenate[int, T, P], T] self.assertEqual(C2.__args__, (Concatenate[int, T, P], T)) - self.assertEqual(C2.__parameters__, (T,)) + self.assertEqual(C2.__parameters__, (T, P)) # Test collections.abc.Callable too. C3 = collections.abc.Callable[Concatenate[int, P], int] self.assertEqual(C3.__args__, (Concatenate[int, P], int)) - self.assertEqual(C3.__parameters__, ()) + self.assertEqual(C3.__parameters__, (P,)) C4 = collections.abc.Callable[Concatenate[int, T, P], T] self.assertEqual(C4.__args__, (Concatenate[int, T, P], T)) - self.assertEqual(C4.__parameters__, (T,)) + self.assertEqual(C4.__parameters__, (T, P)) class AllTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 6936a1052ca7a6..2e1ee4917feb8e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -180,14 +180,14 @@ def _type_repr(obj): def _collect_type_vars(types): - """Collect all type variable contained in types in order of - first appearance (lexicographic order). For example:: + """Collect all type variable and parameter specification variables contained + in types in order of first appearance (lexicographic order). For example:: _collect_type_vars((T, List[S, T])) == (T, S) """ tvars = [] for t in types: - if isinstance(t, TypeVar) and t not in tvars: + if isinstance(t, (TypeVar, ParamSpec)) and t not in tvars: tvars.append(t) if isinstance(t, (_GenericAlias, GenericAlias)): tvars.extend([t for t in t.__parameters__ if t not in tvars]) @@ -529,10 +529,7 @@ def Concatenate(self, parameters): Callable[Concatenate[int, P], int] - .. seealso:: - - :pep:`612` -- Parameter Specification Variables - + See PEP 612 for detailed information. """ if parameters == (): raise TypeError("Cannot take a Concatenate of no types.") @@ -1008,8 +1005,9 @@ def __getitem__(self, params): params = (Ellipsis, result) else: if not isinstance(args, (list, ParamSpec, _ConcatenateGenericAlias)): - raise TypeError(f"Callable[args, result]: args must be a list." - f" Got {args}") + raise TypeError(f"Callable[args, result]: args must be a list, " + f"ParamSpec or Concatenate. " + f"Got {args}") if isinstance(args, list): params = (tuple(args), result) return self.__getitem_inner__(params) @@ -1092,12 +1090,7 @@ def __hash__(self): class _ConcatenateGenericAlias(_GenericAlias, _root=True): - - def __or__(self, right): - return NotImplemented - - def __ror__(self, right): - return NotImplemented + pass class Generic: diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 51a12377b7e308..0a90fe334ee061 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -156,13 +156,14 @@ ga_repr(PyObject *self) return NULL; } -// isinstance(obj, TypeVar) without importing typing.py. +// isinstance(obj, (TypeVar, ParamSpec)) without importing typing.py. // Returns -1 for errors. static int is_typevar(PyObject *obj) { PyTypeObject *type = Py_TYPE(obj); - if (strcmp(type->tp_name, "TypeVar") != 0) { + if ((strcmp(type->tp_name, "TypeVar") & + strcmp(type->tp_name, "ParamSpec")) != 0) { return 0; } PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__"); From d09d0881a33d4779ba91eab78284c52a52ffa24b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:06:10 +0800 Subject: [PATCH 08/24] add tests for user defined generics --- Lib/test/test_typing.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 1b1232b2604b41..938a3e6cb9d229 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4339,6 +4339,43 @@ def test_no_bivariant(self): with self.assertRaises(ValueError): ParamSpec('P', covariant=True, contravariant=True) + def test_user_generics(self): + T = TypeVar("T") + P = ParamSpec("P") + P_2 = ParamSpec("P_2") + + class X(Generic[T, P]): + f: Callable[P, int] + x: T + G1 = X[int, P_2] + self.assertEqual(G1.__args__, (int, P_2)) + self.assertEqual(G1.__parameters__, (P_2,)) + + G2 = X[int, Concatenate[int, P_2]] + self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) + self.assertEqual(G2.__parameters__, (P_2,)) + + # currently raises TypeError for _type_check + # G3 = X[int, [int, bool]] + # self.assertEqual(G3.__args__, (int, [int, bool])) + # self.assertEqual(G3.__parameters__, ()) + + # G4 = X[int, ...] + # self.assertEqual(G4.__args__, (int, type(Ellipsis))) + # self.assertEqual(G4.__parameters__, ()) + # + # class Z(Generic[P]): + # f: Callable[P, int] + + # These are valid + # currently raises TypeError for _type_check + # G5 = Z[[int, str, bool]] + + # currently raises TypeError for too many parameters (not enough TypeVars) + # G6 = Z[int, str, bool] + # self.assertEqual(G6.__args__, (int, str, bool)) + # self.assertEqual(G6.__parameters__, ()) + class ConcatenateTests(BaseTestCase): def test_basics(self): From 9727e2af972fb6c1996a4df78d814284c01b25a3 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 15 Dec 2020 00:22:12 +0800 Subject: [PATCH 09/24] cast list to tuple done, loosened type checks for Generic --- Lib/test/test_typing.py | 49 +++++++++++++++++++---------------------- Lib/typing.py | 20 +++++++++++++++-- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index afb2129030a980..2a899b87ae51d3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1131,10 +1131,6 @@ class P(PR[int, T], Protocol[T]): PR[int] with self.assertRaises(TypeError): P[int, str] - with self.assertRaises(TypeError): - PR[int, 1] - with self.assertRaises(TypeError): - PR[int, ClassVar] class C(PR[int, T]): pass @@ -1156,8 +1152,6 @@ class P(PR[int, str], Protocol): self.assertIsSubclass(P, PR) with self.assertRaises(TypeError): PR[int] - with self.assertRaises(TypeError): - PR[int, 1] class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... @@ -1176,8 +1170,6 @@ def bar(self, x: str) -> str: return x self.assertIsInstance(Test(), PSub) - with self.assertRaises(TypeError): - PR[int, ClassVar] def test_init_called(self): T = TypeVar('T') @@ -4346,25 +4338,30 @@ class X(Generic[T, P]): self.assertEqual(G2.__parameters__, (P_2,)) # currently raises TypeError for _type_check - # G3 = X[int, [int, bool]] - # self.assertEqual(G3.__args__, (int, [int, bool])) - # self.assertEqual(G3.__parameters__, ()) - - # G4 = X[int, ...] - # self.assertEqual(G4.__args__, (int, type(Ellipsis))) - # self.assertEqual(G4.__parameters__, ()) - # - # class Z(Generic[P]): - # f: Callable[P, int] - - # These are valid - # currently raises TypeError for _type_check - # G5 = Z[[int, str, bool]] + G3 = X[int, [int, bool]] + self.assertEqual(G3.__args__, (int, (int, bool))) + self.assertEqual(G3.__parameters__, ()) + + G4 = X[int, ...] + self.assertEqual(G4.__args__, (int, Ellipsis)) + self.assertEqual(G4.__parameters__, ()) + + class Z(Generic[P]): + f: Callable[P, int] + + G5 = Z[[int, str, bool]] + self.assertEqual(G5.__args__, ((int, str, bool),)) + self.assertEqual(G5.__parameters__, ()) + + G6 = Z[int, str, bool] + self.assertEqual(G6.__args__, ((int, str, bool),)) + self.assertEqual(G6.__parameters__, ()) - # currently raises TypeError for too many parameters (not enough TypeVars) - # G6 = Z[int, str, bool] - # self.assertEqual(G6.__args__, (int, str, bool)) - # self.assertEqual(G6.__parameters__, ()) + # G5 and G6 should be equivalent according to the PEP + self.assertEqual(G5.__args__, G6.__args__) + self.assertEqual(G5.__origin__, G6.__origin__) + self.assertEqual(G5.__parameters__, G6.__parameters__) + self.assertEqual(G5, G6) class ConcatenateTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index a5e83687786ba9..cf55464ea7dd10 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1127,8 +1127,7 @@ def __class_getitem__(cls, params): if not params and cls is not Tuple: raise TypeError( f"Parameter list to {cls.__qualname__}[...] cannot be empty") - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) + params = tuple(_type_convert(p) for p in params) if cls in (Generic, Protocol): # Generic and Protocol can only be subscripted with unique type variables. if not all(isinstance(p, (TypeVar, ParamSpec)) for p in params): @@ -1140,6 +1139,23 @@ def __class_getitem__(cls, params): f"Parameters to {cls.__name__}[...] must all be unique") else: # Subscripting a regular Generic subclass. + + # Code below handles PEP 612 ParamSpec. + if any(isinstance(t, ParamSpec) for t in cls.__parameters__): + # Special case where Z[[int, str, bool]] == Z[int, str, bool] + # in PEP 612. + if len(cls.__parameters__) == 1 and len(params) > 1: + params = (params,) + else: + _params = [] + # Convert lists to tuples to help other libraries cache the + # results. + for p, tvar in zip(params, cls.__parameters__): + if isinstance(tvar, ParamSpec) and isinstance(p, list): + p = tuple(p) + _params.append(p) + params = tuple(_params) + _check_generic(cls, params, len(cls.__parameters__)) return _GenericAlias(cls, params) From cc7fc1c44e934566550963e5e25b968e8aaaa78e Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 15 Dec 2020 22:44:09 +0800 Subject: [PATCH 10/24] loosen generics, allow typevar-like subst, flatten out args if Callable --- Lib/test/test_typing.py | 13 ++++++------ Lib/typing.py | 44 +++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2a899b87ae51d3..73573c434fe5e8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1739,8 +1739,6 @@ def test_extended_generic_rules_eq(self): self.assertEqual(typing.Iterable[Tuple[T, T]][T], typing.Iterable[Tuple[T, T]]) with self.assertRaises(TypeError): Tuple[T, int][()] - with self.assertRaises(TypeError): - Tuple[T, U][T, ...] self.assertEqual(Union[T, int][int], int) self.assertEqual(Union[T, U][int, Union[int, str]], Union[int, str]) @@ -1752,10 +1750,6 @@ class Derived(Base): ... self.assertEqual(Callable[[T], T][KT], Callable[[KT], KT]) self.assertEqual(Callable[..., List[T]][int], Callable[..., List[int]]) - with self.assertRaises(TypeError): - Callable[[T], U][..., int] - with self.assertRaises(TypeError): - Callable[[T], U][[], int] def test_extended_generic_rules_repr(self): T = TypeVar('T') @@ -4363,6 +4357,13 @@ class Z(Generic[P]): self.assertEqual(G5.__parameters__, G6.__parameters__) self.assertEqual(G5, G6) + def test_var_substitution(self): + T = TypeVar("T") + P = ParamSpec("P") + C1 = Callable[P, T] + self.assertEqual(C1[int, str], Callable[[int], str]) + self.assertEqual(C1[[int, str, dict], float], Callable[[int, str, dict], float]) + class ConcatenateTests(BaseTestCase): def test_basics(self): diff --git a/Lib/typing.py b/Lib/typing.py index cf55464ea7dd10..fa76adb595ac98 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -212,6 +212,21 @@ def _check_generic(cls, parameters, elen): raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" f" actual {alen}, expected {elen}") +def _prepare_paramspec_params(cls, params): + """Prepares the parameters for a Generic containing ParamSpec + variables (internal helper). + """ + # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. + if len(cls.__parameters__) == 1 and len(params) > 1: + return params, + else: + _params = [] + # Convert lists to tuples to help other libraries cache the results. + for p, tvar in zip(params, cls.__parameters__): + if isinstance(tvar, ParamSpec) and isinstance(p, list): + p = tuple(p) + _params.append(p) + return tuple(_params) def _deduplicate(params): # Weed out strict duplicates, preserving the first of each occurrence. @@ -881,21 +896,26 @@ def __getitem__(self, params): raise TypeError(f"Cannot subscript already-subscripted {self}") if not isinstance(params, tuple): params = (params,) - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) + params = tuple(_type_convert(p) for p in params) + if any(isinstance(t, ParamSpec) for t in self.__parameters__): + params = _prepare_paramspec_params(self, params) _check_generic(self, params, len(self.__parameters__)) subst = dict(zip(self.__parameters__, params)) new_args = [] for arg in self.__args__: - if isinstance(arg, TypeVar): + if isinstance(arg, (TypeVar, ParamSpec)): arg = subst[arg] elif isinstance(arg, (_GenericAlias, GenericAlias)): subparams = arg.__parameters__ if subparams: subargs = tuple(subst[x] for x in subparams) arg = arg[subargs] - new_args.append(arg) + # Required to flatten out the args for CallableGenericAlias + if self.__origin__ == collections.abc.Callable and isinstance(arg, tuple): + new_args.extend(arg) + else: + new_args.append(arg) return self.copy_with(tuple(new_args)) def copy_with(self, params): @@ -1140,22 +1160,8 @@ def __class_getitem__(cls, params): else: # Subscripting a regular Generic subclass. - # Code below handles PEP 612 ParamSpec. if any(isinstance(t, ParamSpec) for t in cls.__parameters__): - # Special case where Z[[int, str, bool]] == Z[int, str, bool] - # in PEP 612. - if len(cls.__parameters__) == 1 and len(params) > 1: - params = (params,) - else: - _params = [] - # Convert lists to tuples to help other libraries cache the - # results. - for p, tvar in zip(params, cls.__parameters__): - if isinstance(tvar, ParamSpec) and isinstance(p, list): - p = tuple(p) - _params.append(p) - params = tuple(_params) - + params = _prepare_paramspec_params(cls, params) _check_generic(cls, params, len(cls.__parameters__)) return _GenericAlias(cls, params) From 3e67f23b58ec0b6f3ad75e5052f00dd54e4f7b3a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 00:01:58 +0800 Subject: [PATCH 11/24] fix whitespace issue, cast list to tuple for types.GenericAlias --- Lib/typing.py | 2 +- Objects/genericaliasobject.c | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index fa76adb595ac98..58e7ff6124942f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1161,7 +1161,7 @@ def __class_getitem__(cls, params): # Subscripting a regular Generic subclass. if any(isinstance(t, ParamSpec) for t in cls.__parameters__): - params = _prepare_paramspec_params(cls, params) + params = _prepare_paramspec_params(cls, params) _check_generic(cls, params, len(cls.__parameters__)) return _GenericAlias(cls, params) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 205bbf4ff74a3a..e22369e45ed271 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -280,7 +280,14 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems) if (iparam >= 0) { arg = argitems[iparam]; } - Py_INCREF(arg); + // convert all the lists inside args to tuples to help + // with caching in other libaries + if (PyList_CheckExact(arg)) { + arg = PyList_AsTuple(arg); + } + else { + Py_INCREF(arg); + } PyTuple_SET_ITEM(subargs, i, arg); } @@ -576,15 +583,29 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) { return 0; } } - else { - Py_INCREF(args); + Py_ssize_t argslen = PyTuple_GET_SIZE(args); + PyObject *_args = PyList_New(argslen); + if (_args == NULL) { + return 0; + } + // convert all the lists inside args to tuples to help + // with caching in other libaries + for (Py_ssize_t i = 0; i < argslen; ++i) { + PyObject *p = PyTuple_GET_ITEM(args, i); + if (PyList_CheckExact(p)) { + p = PyList_AsTuple(p); + } + PyList_SET_ITEM(_args, i, p); } + args = PyList_AsTuple(_args); + PyObject_GC_Del(_args); Py_INCREF(origin); alias->origin = origin; alias->args = args; alias->parameters = NULL; alias->weakreflist = NULL; + return 1; } From d9baa1ab1d07afbc0adad7b9a212b91117ac93e2 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 17:25:42 +0800 Subject: [PATCH 12/24] convert list to tuples if substituting paramspecs in types.GenericAlias --- Lib/_collections_abc.py | 12 ++++- Lib/test/test_genericalias.py | 14 +++++ Objects/genericaliasobject.c | 97 ++++++++++++++++++++++------------- 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 7c3faa64ea7f98..2b72705c6adbf8 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -444,7 +444,8 @@ def __create_ga(cls, origin, args): return super().__new__(cls, origin, ga_args) def __repr__(self): - if len(self.__args__) == 2 and self.__args__[0] is Ellipsis: + if len(self.__args__) == 2 and (self.__args__[0] is Ellipsis + or _is_typing(self.__args__[0])): return super().__repr__() return (f'collections.abc.Callable' f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' @@ -452,11 +453,18 @@ def __repr__(self): def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and args[0] is Ellipsis): + if not (len(args) == 2 and (args[0] is Ellipsis or _is_typing(args[0]))): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) +def _is_typing(obj): + """Checks if obj is from typing.py""" + if isinstance(obj, type): + return obj.__module__ == 'typing' + return type(obj).__module__ == 'typing' + + def _type_repr(obj): """Return the repr() of an object, special-casing types (internal helper). diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 5de13fe6d2f68c..a2c7a94bc2237d 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -363,6 +363,20 @@ def __call__(self): self.assertEqual(c1.__args__, c2.__args__) self.assertEqual(hash(c1.__args__), hash(c2.__args__)) + with self.subTest("Testing ParamSpec uses"): + P = typing.ParamSpec('P') + C1 = Callable[P, T] + # substitution + self.assertEqual(C1[int, str], Callable[[int], str]) + # sadly not yet + # self.assertEqual(C1[[int, str], str], Callable[[int, str], str]) + + C2 = Callable[P, int] + # special case in PEP 612 where + # X[int, str, float] == X[[int, str, float]] + self.assertEqual(C2[int, str, float], C2[[int, str, float]]) + + if __name__ == "__main__": unittest.main() diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index e22369e45ed271..0a039f2d938e62 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -156,14 +156,21 @@ ga_repr(PyObject *self) return NULL; } -// isinstance(obj, (TypeVar, ParamSpec)) without importing typing.py. -// Returns -1 for errors. -static int -is_typevar(PyObject *obj) +/* Checks if a variable number of names are from typing.py. +* If any one of the names are found, return 1, else 0. +**/ +static inline int +is_typing_names(PyObject *obj, int num, ...) { + va_list names; + va_start(names, num); + PyTypeObject *type = Py_TYPE(obj); - if ((strcmp(type->tp_name, "TypeVar") & - strcmp(type->tp_name, "ParamSpec")) != 0) { + int cmp = 1; + for (int i = 0; i < num && cmp != 0; ++i) { + cmp &= strcmp(type->tp_name, va_arg(names, const char *)); + } + if (cmp != 0) { return 0; } PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__"); @@ -173,9 +180,25 @@ is_typevar(PyObject *obj) int res = PyUnicode_Check(module) && _PyUnicode_EqualToASCIIString(module, "typing"); Py_DECREF(module); + + va_end(names); return res; } +// isinstance(obj, (TypeVar, ParamSpec)) without importing typing.py. +// Returns -1 for errors. +static inline int +is_typevarlike(PyObject *obj) +{ + return is_typing_names(obj, 2, "TypeVar", "ParamSpec"); +} + +static inline int +is_paramspec(PyObject *obj) +{ + return is_typing_names(obj, 1, "ParamSpec"); +} + // Index of item in self[:len], or -1 if not found (self is a tuple) static Py_ssize_t tuple_index(PyObject *self, Py_ssize_t len, PyObject *item) @@ -210,7 +233,7 @@ make_parameters(PyObject *args) Py_ssize_t iparam = 0; for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { PyObject *t = PyTuple_GET_ITEM(args, iarg); - int typevar = is_typevar(t); + int typevar = is_typevarlike(t); if (typevar < 0) { Py_DECREF(parameters); return NULL; @@ -276,19 +299,20 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems) } for (Py_ssize_t i = 0; i < nsubargs; ++i) { PyObject *arg = PyTuple_GET_ITEM(subparams, i); + PyObject *subst = arg; Py_ssize_t iparam = tuple_index(params, nparams, arg); if (iparam >= 0) { - arg = argitems[iparam]; + subst = argitems[iparam]; } // convert all the lists inside args to tuples to help - // with caching in other libaries - if (PyList_CheckExact(arg)) { - arg = PyList_AsTuple(arg); + // with caching in other libaries if substituting a ParamSpec + if (PyList_CheckExact(subst) && is_paramspec(arg)) { + subst = PyList_AsTuple(subst); } else { - Py_INCREF(arg); + Py_INCREF(subst); } - PyTuple_SET_ITEM(subargs, i, arg); + PyTuple_SET_ITEM(subargs, i, subst); } obj = PyObject_GetItem(obj, subargs); @@ -322,11 +346,19 @@ ga_getitem(PyObject *self, PyObject *item) int is_tuple = PyTuple_Check(item); Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1; PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item; - if (nitems != nparams) { - return PyErr_Format(PyExc_TypeError, - "Too %s arguments for %R", - nitems > nparams ? "many" : "few", - self); + // A special case in PEP 612 where if X = Callable[P, int], + // then X[int, str] == X[[int, str], int]. + if (nparams == 1 && nitems > 1 && is_tuple && + is_paramspec(PyTuple_GET_ITEM(alias->parameters, 0))) { + argitems = &item; + } + else { + if (nitems != nparams) { + return PyErr_Format(PyExc_TypeError, + "Too %s arguments for %R", + nitems > nparams ? "many" : "few", + self); + } } /* Replace all type variables (specified by alias->parameters) with corresponding values specified by argitems. @@ -341,7 +373,8 @@ ga_getitem(PyObject *self, PyObject *item) } for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg); - int typevar = is_typevar(arg); + int typevar = is_typevarlike(arg); + int paramspec = is_paramspec(arg); if (typevar < 0) { Py_DECREF(newargs); return NULL; @@ -350,7 +383,14 @@ ga_getitem(PyObject *self, PyObject *item) Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg); assert(iparam >= 0); arg = argitems[iparam]; - Py_INCREF(arg); + // convert all the lists inside args to tuples to help + // with caching in other libaries if substituting a ParamSpec + if (PyList_CheckExact(arg) && paramspec) { + arg = PyList_AsTuple(arg); + } + else { + Py_INCREF(arg); + } } else { arg = subs_tvars(arg, alias->parameters, argitems); @@ -583,22 +623,9 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) { return 0; } } - Py_ssize_t argslen = PyTuple_GET_SIZE(args); - PyObject *_args = PyList_New(argslen); - if (_args == NULL) { - return 0; - } - // convert all the lists inside args to tuples to help - // with caching in other libaries - for (Py_ssize_t i = 0; i < argslen; ++i) { - PyObject *p = PyTuple_GET_ITEM(args, i); - if (PyList_CheckExact(p)) { - p = PyList_AsTuple(p); - } - PyList_SET_ITEM(_args, i, p); + else { + Py_INCREF(args); } - args = PyList_AsTuple(_args); - PyObject_GC_Del(_args); Py_INCREF(origin); alias->origin = origin; From c4155b63c55530a23169ff82964740c21336611e Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 17:40:25 +0800 Subject: [PATCH 13/24] done! flattened __args__ in substitutions for collections.abc.Callable --- Lib/_collections_abc.py | 15 +++++++++++++++ Lib/test/test_genericalias.py | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 2b72705c6adbf8..76d665fb603f03 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -457,6 +457,21 @@ def __reduce__(self): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) + def __getitem__(self, item): + # To allow the following:: + # C1 = Callable[P, T] + # C1[[int, str], str] == Callable[[int, str], str] + # Where P is a PEP 612 ParamSpec. + ga = super().__getitem__(item) + new_args = [] + # flatten args + if isinstance(ga.__args__[0], tuple): + new_args.extend(ga.__args__[0]) + new_args.extend(ga.__args__[1:]) + else: + new_args = ga.__args__ + return GenericAlias(Callable, tuple(new_args)) + def _is_typing(obj): """Checks if obj is from typing.py""" diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index a2c7a94bc2237d..a7540afacec57a 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -368,14 +368,16 @@ def __call__(self): C1 = Callable[P, T] # substitution self.assertEqual(C1[int, str], Callable[[int], str]) - # sadly not yet - # self.assertEqual(C1[[int, str], str], Callable[[int, str], str]) + self.assertEqual(C1[[int, str], str], Callable[[int, str], str]) C2 = Callable[P, int] # special case in PEP 612 where # X[int, str, float] == X[[int, str, float]] self.assertEqual(C2[int, str, float], C2[[int, str, float]]) + with self.subTest("Testing Concatenate uses"): + P = typing.ParamSpec('P') + Callable[typing.Concatenate[int, P], int] if __name__ == "__main__": From 2dbf86145caaa9947c4db82e26e375f5041e793c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 22:02:36 +0800 Subject: [PATCH 14/24] fix repr problems, add repr tests --- Lib/_collections_abc.py | 32 ++++++++++++++++---------------- Lib/test/test_genericalias.py | 9 +++++++-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 76d665fb603f03..421242fb5b212e 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -434,7 +434,7 @@ def __create_ga(cls, origin, args): raise TypeError( "Callable must be used as Callable[[arg, ...], result].") t_args, t_result = args - if isinstance(t_args, list): + if isinstance(t_args, (list, tuple)): ga_args = tuple(t_args) + (t_result,) # This relaxes what t_args can be on purpose to allow things like # PEP 612 ParamSpec. Responsibility for whether a user is using @@ -445,7 +445,7 @@ def __create_ga(cls, origin, args): def __repr__(self): if len(self.__args__) == 2 and (self.__args__[0] is Ellipsis - or _is_typing(self.__args__[0])): + or _concat_or_paramspec(self.__args__[0])): return super().__repr__() return (f'collections.abc.Callable' f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' @@ -453,7 +453,8 @@ def __repr__(self): def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and (args[0] is Ellipsis or _is_typing(args[0]))): + if not (len(args) == 2 and (args[0] is Ellipsis + or _concat_or_paramspec(args[0]))): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) @@ -463,22 +464,21 @@ def __getitem__(self, item): # C1[[int, str], str] == Callable[[int, str], str] # Where P is a PEP 612 ParamSpec. ga = super().__getitem__(item) - new_args = [] - # flatten args - if isinstance(ga.__args__[0], tuple): - new_args.extend(ga.__args__[0]) - new_args.extend(ga.__args__[1:]) - else: - new_args = ga.__args__ - return GenericAlias(Callable, tuple(new_args)) + args = ga.__args__ + if not isinstance(ga.__args__[0], tuple): + t_result = ga.__args__[-1] + t_args = ga.__args__[:-1] + args = (t_args, t_result) + return _CallableGenericAlias(Callable, args) -def _is_typing(obj): - """Checks if obj is from typing.py""" - if isinstance(obj, type): - return obj.__module__ == 'typing' - return type(obj).__module__ == 'typing' +def _is_typing_names(obj, names): + """Checks if obj matches one of the names in *names* in typing.py""" + obj = type(obj) + return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names) +def _concat_or_paramspec(obj): + return _is_typing_names(obj, ('ParamSpec', '_ConcatenateGenericAlias')) def _type_repr(obj): """Return the repr() of an object, special-casing types (internal helper). diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index a7540afacec57a..a23c29fcec6b75 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -369,16 +369,21 @@ def __call__(self): # substitution self.assertEqual(C1[int, str], Callable[[int], str]) self.assertEqual(C1[[int, str], str], Callable[[int, str], str]) + self.assertEqual(repr(C1).split(".")[-1], "Callable[~P, ~T]") + self.assertEqual(repr(C1[int, str]).split(".")[-1], "Callable[[int], str]") C2 = Callable[P, int] # special case in PEP 612 where # X[int, str, float] == X[[int, str, float]] self.assertEqual(C2[int, str, float], C2[[int, str, float]]) + self.assertEqual(repr(C2).split(".")[-1], "Callable[~P, int]") + self.assertEqual(repr(C2[int, str]).split(".")[-1], "Callable[[int, str], int]") with self.subTest("Testing Concatenate uses"): P = typing.ParamSpec('P') - Callable[typing.Concatenate[int, P], int] - + C1 = Callable[typing.Concatenate[int, P], int] + self.assertEqual(repr(C1), "collections.abc.Callable" + "[typing.Concatenate[int, ~P], int]") if __name__ == "__main__": unittest.main() From 87c2d192ad6289fa3ee1d13f751f489ffeb01582 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 22:22:27 +0800 Subject: [PATCH 15/24] Add another test for multiple chaining --- Lib/test/test_genericalias.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index a23c29fcec6b75..5579b7d372c417 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -347,6 +347,9 @@ def test_abc_callable(self): self.assertEqual(C2[int, float, str], Callable[[int, float], str]) self.assertEqual(C3[int], Callable[..., int]) + # mutli chaining + self.assertEqual(C2[int, V, str][dict], Callable[[int, dict], str]) + with self.subTest("Testing type erasure"): class C1(Callable): def __call__(self): From 2b09de653150b45910da43474922ccd8a41caafe Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 16 Dec 2020 22:23:00 +0800 Subject: [PATCH 16/24] fix typo --- Lib/test/test_genericalias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 5579b7d372c417..0cac7e0c76058d 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -347,7 +347,7 @@ def test_abc_callable(self): self.assertEqual(C2[int, float, str], Callable[[int, float], str]) self.assertEqual(C3[int], Callable[..., int]) - # mutli chaining + # multi chaining self.assertEqual(C2[int, V, str][dict], Callable[[int, dict], str]) with self.subTest("Testing type erasure"): From d98070238ff1c2cf723ed2a766c6ba90bc992b74 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 17 Dec 2020 23:50:43 +0800 Subject: [PATCH 17/24] Clean up some comments --- Lib/_collections_abc.py | 2 +- Objects/genericaliasobject.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 421242fb5b212e..f05f020830e380 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -460,9 +460,9 @@ def __reduce__(self): def __getitem__(self, item): # To allow the following:: + # P = ParamSpec('P) # C1 = Callable[P, T] # C1[[int, str], str] == Callable[[int, str], str] - # Where P is a PEP 612 ParamSpec. ga = super().__getitem__(item) args = ga.__args__ if not isinstance(ga.__args__[0], tuple): diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 0a039f2d938e62..7501202b18eb47 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -347,7 +347,7 @@ ga_getitem(PyObject *self, PyObject *item) Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1; PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item; // A special case in PEP 612 where if X = Callable[P, int], - // then X[int, str] == X[[int, str], int]. + // then X[int, str] == X[[int, str]]. if (nparams == 1 && nitems > 1 && is_tuple && is_paramspec(PyTuple_GET_ITEM(alias->parameters, 0))) { argitems = &item; @@ -383,8 +383,8 @@ ga_getitem(PyObject *self, PyObject *item) Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg); assert(iparam >= 0); arg = argitems[iparam]; - // convert all the lists inside args to tuples to help - // with caching in other libaries if substituting a ParamSpec + // If substituting a ParamSpec, convert lists to tuples to help + // with caching in other libaries. if (PyList_CheckExact(arg) && paramspec) { arg = PyList_AsTuple(arg); } From d6f777c0305b04466f158d6d23c9c72761ac26cf Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 23 Dec 2020 00:50:06 +0800 Subject: [PATCH 18/24] remove stray whitespace --- Objects/genericaliasobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 7501202b18eb47..0d71bc52ea310e 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -632,7 +632,6 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) { alias->args = args; alias->parameters = NULL; alias->weakreflist = NULL; - return 1; } From 9a8176b7c3af3884242748cf793b7cc9ed4d34b7 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 23 Dec 2020 13:06:46 +0800 Subject: [PATCH 19/24] Address nearly all of Guido's reviews Co-Authored-By: Guido van Rossum --- Lib/_collections_abc.py | 20 +++++++++++--------- Lib/test/test_typing.py | 39 ++++----------------------------------- Lib/typing.py | 10 +++++----- 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index f05f020830e380..dc9a06c5d285ee 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -416,7 +416,7 @@ def __subclasshook__(cls, C): class _CallableGenericAlias(GenericAlias): """ Represent `Callable[argtypes, resulttype]`. - This sets ``__args__`` to a tuple containing the flattened``argtypes`` + This sets ``__args__`` to a tuple containing the flattened ``argtypes`` followed by ``resulttype``. Example: ``Callable[[int, str], float]`` sets ``__args__`` to @@ -444,8 +444,7 @@ def __create_ga(cls, origin, args): return super().__new__(cls, origin, ga_args) def __repr__(self): - if len(self.__args__) == 2 and (self.__args__[0] is Ellipsis - or _concat_or_paramspec(self.__args__[0])): + if len(self.__args__) == 2 and _has_special_args(self.__args__[0]): return super().__repr__() return (f'collections.abc.Callable' f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' @@ -453,8 +452,7 @@ def __repr__(self): def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and (args[0] is Ellipsis - or _concat_or_paramspec(args[0]))): + if not (len(args) == 2 and _has_special_args(args[0])): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) @@ -465,6 +463,7 @@ def __getitem__(self, item): # C1[[int, str], str] == Callable[[int, str], str] ga = super().__getitem__(item) args = ga.__args__ + # args[0] occurs due to things like Z[[int, str, bool]] from PEP 612 if not isinstance(ga.__args__[0], tuple): t_result = ga.__args__[-1] t_args = ga.__args__[:-1] @@ -472,13 +471,16 @@ def __getitem__(self, item): return _CallableGenericAlias(Callable, args) -def _is_typing_names(obj, names): - """Checks if obj matches one of the names in *names* in typing.py""" +def _has_special_args(obj): + """Checks if obj matches either ``...``, 'ParamSpec' or + '_ConcatenateGenericAlias' from typing.py + """ + if obj is Ellipsis: + return True obj = type(obj) + names = ('ParamSpec', '_ConcatenateGenericAlias') return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names) -def _concat_or_paramspec(obj): - return _is_typing_names(obj, ('ParamSpec', '_ConcatenateGenericAlias')) def _type_repr(obj): """Return the repr() of an object, special-casing types (internal helper). diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 73573c434fe5e8..bef886ab0530a3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4264,26 +4264,18 @@ def test_instance_type_error(self): P = ParamSpec('P') with self.assertRaises(TypeError): isinstance(42, P) - - def test_instance_type_error(self): - P = ParamSpec('P') with self.assertRaises(TypeError): issubclass(int, P) with self.assertRaises(TypeError): issubclass(P, int) - def test_union_unique(self): + def test_unique(self): P1 = ParamSpec('P1') P2 = ParamSpec('P2') self.assertNotEqual(P1, P2) - self.assertEqual(Union[P1], P1) - self.assertNotEqual(Union[P1], Union[P1, P2]) - self.assertEqual(Union[P1, P1], P1) - self.assertNotEqual(Union[P1, int], Union[P1]) - self.assertNotEqual(Union[P1, int], Union[int]) - self.assertEqual(Union[P1, int].__args__, (P1, int)) - self.assertEqual(Union[P1, int].__parameters__, (P1,)) - self.assertIs(Union[P1, int].__origin__, Union) + self.assertEqual(Callable[P1, int].__args__, (P1, int)) + self.assertEqual(Union[Callable[P1, int], Callable[P1, int]].__parameters__, (P1,)) + self.assertEqual(Union[Callable[P1, int], Callable[P2, int]].__parameters__, (P1, P2)) def test_repr(self): P = ParamSpec('P') @@ -4293,28 +4285,6 @@ def test_repr(self): P_contra = ParamSpec('P_contra', contravariant=True) self.assertEqual(repr(P_contra), '-P_contra') - def test_no_redefinition(self): - self.assertNotEqual(ParamSpec('P'), ParamSpec('P')) - self.assertNotEqual(ParamSpec('P', int, str), ParamSpec('P', int, str)) - - def test_cannot_subclass_vars(self): - with self.assertRaises(TypeError): - class V(ParamSpec('P')): - pass - - def test_cannot_subclass_var_itself(self): - with self.assertRaises(TypeError): - class V(ParamSpec): - pass - - def test_cannot_instantiate_vars(self): - with self.assertRaises(TypeError): - ParamSpec('A')() - - def test_no_bivariant(self): - with self.assertRaises(ValueError): - ParamSpec('P', covariant=True, contravariant=True) - def test_user_generics(self): T = TypeVar("T") P = ParamSpec("P") @@ -4331,7 +4301,6 @@ class X(Generic[T, P]): self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) self.assertEqual(G2.__parameters__, (P_2,)) - # currently raises TypeError for _type_check G3 = X[int, [int, bool]] self.assertEqual(G3.__args__, (int, (int, bool))) self.assertEqual(G3.__parameters__, ()) diff --git a/Lib/typing.py b/Lib/typing.py index 58e7ff6124942f..c84f8050bf74a2 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -187,14 +187,14 @@ def _type_repr(obj): def _collect_type_vars(types): - """Collect all type variable and parameter specification variables contained + """Collect all type variable-like variables contained in types in order of first appearance (lexicographic order). For example:: _collect_type_vars((T, List[S, T])) == (T, S) """ tvars = [] for t in types: - if isinstance(t, (TypeVar, ParamSpec)) and t not in tvars: + if isinstance(t, _TypeVarLike) and t not in tvars: tvars.append(t) if isinstance(t, (_GenericAlias, GenericAlias)): tvars.extend([t for t in t.__parameters__ if t not in tvars]) @@ -218,7 +218,7 @@ def _prepare_paramspec_params(cls, params): """ # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. if len(cls.__parameters__) == 1 and len(params) > 1: - return params, + return (params,) else: _params = [] # Convert lists to tuples to help other libraries cache the results. @@ -557,11 +557,11 @@ def Concatenate(self, parameters): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - msg = "Concatenate[arg, ...]: each arg must be a type." - parameters = tuple(_type_check(p, msg) for p in parameters) if not isinstance(parameters[-1], ParamSpec): raise TypeError("The last parameter to Concatenate should be a " "ParamSpec variable.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = tuple(_type_check(p, msg) for p in parameters) return _ConcatenateGenericAlias(self, parameters) From fa06838ce83cf72aecc1db5e3fb4fe59f3925b70 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 23 Dec 2020 14:53:00 +0800 Subject: [PATCH 20/24] more reviews; fix some docstrings, clean up code, cast list to tuple by default --- Lib/_collections_abc.py | 4 ++-- Lib/typing.py | 13 ++++++------- Objects/genericaliasobject.c | 35 ++++++++++++++++++----------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index dc9a06c5d285ee..8d4757370928a9 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -472,8 +472,8 @@ def __getitem__(self, item): def _has_special_args(obj): - """Checks if obj matches either ``...``, 'ParamSpec' or - '_ConcatenateGenericAlias' from typing.py + """Checks if obj matches either ``...``, ``ParamSpec`` or + ``_ConcatenateGenericAlias`` from typing.py """ if obj is Ellipsis: return True diff --git a/Lib/typing.py b/Lib/typing.py index c84f8050bf74a2..7b79876d4ebc70 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -736,10 +736,10 @@ class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): Parameter specification variables exist primarily for the benefit of static type checkers. They are used to forward the parameter types of one Callable to another Callable, a pattern commonly found in higher order - functions and decorators. They are only valid as the first argument to - Callable, or as parameters for user-defined Generics. See class Generic - for more information on generic types. An example for annotating a - decorator:: + functions and decorators. They are only valid when used in Concatenate, or + as the first argument to Callable, or as parameters for user-defined Generics. + See class Generic for more information on generic types. An example for + annotating a decorator:: T = TypeVar('T') P = ParamSpec('P') @@ -904,7 +904,7 @@ def __getitem__(self, params): subst = dict(zip(self.__parameters__, params)) new_args = [] for arg in self.__args__: - if isinstance(arg, (TypeVar, ParamSpec)): + if isinstance(arg, _TypeVarLike): arg = subst[arg] elif isinstance(arg, (_GenericAlias, GenericAlias)): subparams = arg.__parameters__ @@ -1150,7 +1150,7 @@ def __class_getitem__(cls, params): params = tuple(_type_convert(p) for p in params) if cls in (Generic, Protocol): # Generic and Protocol can only be subscripted with unique type variables. - if not all(isinstance(p, (TypeVar, ParamSpec)) for p in params): + if not all(isinstance(p, _TypeVarLike) for p in params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be type variables " f"or parameter specification variables.") @@ -1159,7 +1159,6 @@ def __class_getitem__(cls, params): f"Parameters to {cls.__name__}[...] must all be unique") else: # Subscripting a regular Generic subclass. - if any(isinstance(t, ParamSpec) for t in cls.__parameters__): params = _prepare_paramspec_params(cls, params) _check_generic(cls, params, len(cls.__parameters__)) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 0d71bc52ea310e..2d749aeed6eac8 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -160,17 +160,20 @@ ga_repr(PyObject *self) * If any one of the names are found, return 1, else 0. **/ static inline int -is_typing_names(PyObject *obj, int num, ...) +is_typing_name(PyObject *obj, int num, ...) { va_list names; va_start(names, num); PyTypeObject *type = Py_TYPE(obj); - int cmp = 1; - for (int i = 0; i < num && cmp != 0; ++i) { - cmp &= strcmp(type->tp_name, va_arg(names, const char *)); + int hit = 0; + for (int i = 0; i < num; ++i) { + if (!strcmp(type->tp_name, va_arg(names, const char *))) { + hit = 1; + break; + } } - if (cmp != 0) { + if (!hit) { return 0; } PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__"); @@ -190,13 +193,13 @@ is_typing_names(PyObject *obj, int num, ...) static inline int is_typevarlike(PyObject *obj) { - return is_typing_names(obj, 2, "TypeVar", "ParamSpec"); + return is_typing_name(obj, 2, "TypeVar", "ParamSpec"); } static inline int is_paramspec(PyObject *obj) { - return is_typing_names(obj, 1, "ParamSpec"); + return is_typing_name(obj, 1, "ParamSpec"); } // Index of item in self[:len], or -1 if not found (self is a tuple) @@ -302,17 +305,17 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems) PyObject *subst = arg; Py_ssize_t iparam = tuple_index(params, nparams, arg); if (iparam >= 0) { - subst = argitems[iparam]; + arg = argitems[iparam]; } // convert all the lists inside args to tuples to help - // with caching in other libaries if substituting a ParamSpec - if (PyList_CheckExact(subst) && is_paramspec(arg)) { - subst = PyList_AsTuple(subst); + // with caching in other libaries + if (PyList_CheckExact(arg)) { + arg = PyList_AsTuple(arg); } else { - Py_INCREF(subst); + Py_INCREF(arg); } - PyTuple_SET_ITEM(subargs, i, subst); + PyTuple_SET_ITEM(subargs, i, arg); } obj = PyObject_GetItem(obj, subargs); @@ -374,7 +377,6 @@ ga_getitem(PyObject *self, PyObject *item) for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg); int typevar = is_typevarlike(arg); - int paramspec = is_paramspec(arg); if (typevar < 0) { Py_DECREF(newargs); return NULL; @@ -383,9 +385,8 @@ ga_getitem(PyObject *self, PyObject *item) Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg); assert(iparam >= 0); arg = argitems[iparam]; - // If substituting a ParamSpec, convert lists to tuples to help - // with caching in other libaries. - if (PyList_CheckExact(arg) && paramspec) { + // convert lists to tuples to help with caching in other libaries. + if (PyList_CheckExact(arg)) { arg = PyList_AsTuple(arg); } else { From b8672cdc0346f35a4217c2c6d2ebb9af0f420a6e Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:44:06 +0800 Subject: [PATCH 21/24] remove uneeded tests copied over from typevar --- Lib/test/test_typing.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index bef886ab0530a3..c340c8a898289d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4260,31 +4260,6 @@ def test_valid_uses(self): P.args P.kwargs - def test_instance_type_error(self): - P = ParamSpec('P') - with self.assertRaises(TypeError): - isinstance(42, P) - with self.assertRaises(TypeError): - issubclass(int, P) - with self.assertRaises(TypeError): - issubclass(P, int) - - def test_unique(self): - P1 = ParamSpec('P1') - P2 = ParamSpec('P2') - self.assertNotEqual(P1, P2) - self.assertEqual(Callable[P1, int].__args__, (P1, int)) - self.assertEqual(Union[Callable[P1, int], Callable[P1, int]].__parameters__, (P1,)) - self.assertEqual(Union[Callable[P1, int], Callable[P2, int]].__parameters__, (P1, P2)) - - def test_repr(self): - P = ParamSpec('P') - self.assertEqual(repr(P), '~P') - P_co = ParamSpec('P_co', covariant=True) - self.assertEqual(repr(P_co), '+P_co') - P_contra = ParamSpec('P_contra', contravariant=True) - self.assertEqual(repr(P_contra), '-P_contra') - def test_user_generics(self): T = TypeVar("T") P = ParamSpec("P") From 51a463cfbaea2ab4ce29d14a1a2505b35e3d40e6 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 23 Dec 2020 17:03:32 +0800 Subject: [PATCH 22/24] remove unused variable --- Objects/genericaliasobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 2d749aeed6eac8..4cc82ffcdf39a3 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -302,7 +302,6 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems) } for (Py_ssize_t i = 0; i < nsubargs; ++i) { PyObject *arg = PyTuple_GET_ITEM(subparams, i); - PyObject *subst = arg; Py_ssize_t iparam = tuple_index(params, nparams, arg); if (iparam >= 0) { arg = argitems[iparam]; From c05d5d7a0a4e9ab91a7610c69f3ee0c86ccce78c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 24 Dec 2020 10:58:16 +0800 Subject: [PATCH 23/24] merge length checking into _has_special_args too --- Lib/_collections_abc.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 5373dda284dbc9..233eeefdb6f09b 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -444,7 +444,7 @@ def __create_ga(cls, origin, args): return super().__new__(cls, origin, ga_args) def __repr__(self): - if len(self.__args__) == 2 and _has_special_args(self.__args__[0]): + if _has_special_args(self.__args__): return super().__repr__() return (f'collections.abc.Callable' f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' @@ -452,7 +452,7 @@ def __repr__(self): def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and _has_special_args(args[0])): + if not _has_special_args(args): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) @@ -469,10 +469,13 @@ def __getitem__(self, item): return _CallableGenericAlias(Callable, args) -def _has_special_args(obj): - """Checks if obj matches either ``...``, ``ParamSpec`` or +def _has_special_args(args): + """Checks if args[0] matches either ``...``, ``ParamSpec`` or ``_ConcatenateGenericAlias`` from typing.py """ + if not (len(args) == 2): + return False + obj = args[0] if obj is Ellipsis: return True obj = type(obj) From c49ba30c2e6b5f6d1c7ec0d5012bfdda51ebcd16 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 23 Dec 2020 19:07:22 -0800 Subject: [PATCH 24/24] Update Lib/_collections_abc.py --- Lib/_collections_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 233eeefdb6f09b..87302ac76d8019 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -473,7 +473,7 @@ def _has_special_args(args): """Checks if args[0] matches either ``...``, ``ParamSpec`` or ``_ConcatenateGenericAlias`` from typing.py """ - if not (len(args) == 2): + if len(args) != 2: return False obj = args[0] if obj is Ellipsis: