From 73607be68668ab7f4bee53507c8dc7b5a46c9cb4 Mon Sep 17 00:00:00 2001 From: kj <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 24 Dec 2020 12:33:48 +0800 Subject: [PATCH] bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702) --- Lib/_collections_abc.py | 28 ++- Lib/test/test_genericalias.py | 21 ++ Lib/test/test_typing.py | 120 ++++++++-- Lib/typing.py | 209 ++++++++++++++---- .../2020-12-10-00-09-40.bpo-41559.1l4yjP.rst | 2 + Objects/genericaliasobject.c | 76 +++++-- 6 files changed, 381 insertions(+), 75 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index e4eac796462..87302ac76d8 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -416,7 +416,7 @@ class Collection(Sized, Iterable, Container): 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,7 +444,7 @@ class _CallableGenericAlias(GenericAlias): return super().__new__(cls, origin, ga_args) def __repr__(self): - if len(self.__args__) == 2 and self.__args__[0] is Ellipsis: + 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 @@ class _CallableGenericAlias(GenericAlias): def __reduce__(self): args = self.__args__ - if not (len(args) == 2 and args[0] is Ellipsis): + if not _has_special_args(args): args = list(args[:-1]), args[-1] return _CallableGenericAlias, (Callable, args) @@ -461,12 +461,28 @@ class _CallableGenericAlias(GenericAlias): # rather than the default types.GenericAlias object. ga = super().__getitem__(item) args = ga.__args__ - t_result = args[-1] - t_args = args[:-1] - args = (t_args, t_result) + # 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] + args = (t_args, t_result) return _CallableGenericAlias(Callable, args) +def _has_special_args(args): + """Checks if args[0] matches either ``...``, ``ParamSpec`` or + ``_ConcatenateGenericAlias`` from typing.py + """ + if len(args) != 2: + return False + obj = args[0] + 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 _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 ccf40b13d3a..fd024dcec82 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -369,6 +369,27 @@ class BaseTest(unittest.TestCase): 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]) + 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') + C1 = Callable[typing.Concatenate[int, P], int] + self.assertEqual(repr(C1), "collections.abc.Callable" + "[typing.Concatenate[int, ~P], int]") if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8e86e769a0d..c340c8a8982 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -25,6 +25,7 @@ from typing import IO, TextIO, BinaryIO 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 @@ -1130,10 +1131,6 @@ class ProtocolTests(BaseTestCase): 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 @@ -1155,8 +1152,6 @@ class ProtocolTests(BaseTestCase): 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: ... @@ -1175,8 +1170,6 @@ class ProtocolTests(BaseTestCase): return x self.assertIsInstance(Test(), PSub) - with self.assertRaises(TypeError): - PR[int, ClassVar] def test_init_called(self): T = TypeVar('T') @@ -1746,8 +1739,6 @@ class GenericTests(BaseTestCase): 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]) @@ -1759,10 +1750,6 @@ class GenericTests(BaseTestCase): 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') @@ -4243,6 +4230,111 @@ class TypeAliasTests(BaseTestCase): 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__, (P,)) + C2 = Callable[P, T] + self.assertEqual(C2.__args__, (P, 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__, (P,)) + C4 = collections.abc.Callable[P, T] + self.assertEqual(C4.__args__, (P, T)) + self.assertEqual(C4.__parameters__, (P, 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_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,)) + + 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__, ()) + + # 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) + + 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): + 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__, (P,)) + C2 = Callable[Concatenate[int, T, P], T] + self.assertEqual(C2.__args__, (Concatenate[int, T, P], 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__, (P,)) + C4 = collections.abc.Callable[Concatenate[int, T, P], T] + self.assertEqual(C4.__args__, (Concatenate[int, T, P], T)) + self.assertEqual(C4.__parameters__, (T, P)) + + class AllTests(BaseTestCase): """Tests for __all__.""" diff --git a/Lib/typing.py b/Lib/typing.py index 7f07321cda8..7b79876d4eb 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -4,8 +4,10 @@ The typing module: Support for gradual typing as defined by PEP 484. 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 +* 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 +38,13 @@ __all__ = [ 'Any', 'Callable', 'ClassVar', + 'Concatenate', 'Final', 'ForwardRef', 'Generic', 'Literal', 'Optional', + 'ParamSpec', 'Protocol', 'Tuple', 'Type', @@ -154,7 +158,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}.") @@ -183,14 +187,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-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) 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]) @@ -208,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. @@ -523,6 +542,29 @@ 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 adds, removes or transforms parameters of a Callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """ + if parameters == (): + raise TypeError("Cannot take a Concatenate of no types.") + if not isinstance(parameters, tuple): + parameters = (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) + + class ForwardRef(_Final, _root=True): """Internal wrapper to hold a forward reference.""" @@ -585,8 +627,41 @@ class ForwardRef(_Final, _root=True): def __repr__(self): return f'ForwardRef({self.__forward_arg__!r})' +class _TypeVarLike: + """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" + def __init__(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:: @@ -636,20 +711,13 @@ class TypeVar(_Final, _Immutable, _root=True): 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) + 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: 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): @@ -657,23 +725,68 @@ class TypeVar(_Final, _Immutable, _root=True): 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 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 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') + + def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''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) + 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 + super().__init__(bound, covariant, 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): @@ -783,21 +896,26 @@ class _GenericAlias(_BaseGenericAlias, _root=True): 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, _TypeVarLike): 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): @@ -884,15 +1002,18 @@ class _SpecialGenericAlias(_BaseGenericAlias, _root=True): 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) @@ -992,6 +1113,10 @@ class _LiteralGenericAlias(_GenericAlias, _root=True): return hash(frozenset(_value_and_type_iter(self.__args__))) +class _ConcatenateGenericAlias(_GenericAlias, _root=True): + pass + + class Generic: """Abstract base class for generic types. @@ -1022,18 +1147,20 @@ class Generic: 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) 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"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") 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__)) return _GenericAlias(cls, params) 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 00000000000..539fdeccd14 --- /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. diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 756a7ce474a..4cc82ffcdf3 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -156,13 +156,24 @@ error: return NULL; } -// isinstance(obj, TypeVar) 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_name(PyObject *obj, int num, ...) { + va_list names; + va_start(names, num); + PyTypeObject *type = Py_TYPE(obj); - if (strcmp(type->tp_name, "TypeVar") != 0) { + int hit = 0; + for (int i = 0; i < num; ++i) { + if (!strcmp(type->tp_name, va_arg(names, const char *))) { + hit = 1; + break; + } + } + if (!hit) { return 0; } PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__"); @@ -172,9 +183,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_name(obj, 2, "TypeVar", "ParamSpec"); +} + +static inline int +is_paramspec(PyObject *obj) +{ + return is_typing_name(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) @@ -209,7 +236,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; @@ -279,7 +306,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); } @@ -314,11 +348,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]]. + 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. @@ -333,7 +375,7 @@ 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); if (typevar < 0) { Py_DECREF(newargs); return NULL; @@ -342,7 +384,13 @@ 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 lists to tuples to help with caching in other libaries. + if (PyList_CheckExact(arg)) { + arg = PyList_AsTuple(arg); + } + else { + Py_INCREF(arg); + } } else { arg = subs_tvars(arg, alias->parameters, argitems);