[Python-checkins] bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702)

gvanrossum webhook-mailer at python.org
Wed Dec 23 23:33:57 EST 2020


https://github.com/python/cpython/commit/73607be68668ab7f4bee53507c8dc7b5a46c9cb4
commit: 73607be68668ab7f4bee53507c8dc7b5a46c9cb4
branch: master
author: kj <28750310+Fidget-Spinner at users.noreply.github.com>
committer: gvanrossum <gvanrossum at gmail.com>
date: 2020-12-23T20:33:48-08:00
summary:

bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702)

files:
A Misc/NEWS.d/next/Library/2020-12-10-00-09-40.bpo-41559.1l4yjP.rst
M Lib/_collections_abc.py
M Lib/test/test_genericalias.py
M Lib/test/test_typing.py
M Lib/typing.py
M Objects/genericaliasobject.c

diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py
index e4eac79646235..87302ac76d801 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,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 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 @@ def __repr__(self):
 
     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 @@ def __getitem__(self, item):
         # 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 ccf40b13d3a94..fd024dcec8208 100644
--- a/Lib/test/test_genericalias.py
+++ b/Lib/test/test_genericalias.py
@@ -369,6 +369,27 @@ 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])
+            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 8e86e769a0d83..c340c8a898289 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
@@ -1130,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
 
@@ -1155,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: ...
@@ -1175,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')
@@ -1746,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])
@@ -1759,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')
@@ -4243,6 +4230,111 @@ 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__, (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 7f07321cda82a..7b79876d4ebc7 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
-* 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 @@
     '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")
 
 
+ at _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 @@ def __hash__(self):
     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
+
+    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, _root=True):
+class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True):
     """Type variable.
 
     Usage::
@@ -636,20 +711,13 @@ 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)
+        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 @@ 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 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 @@ 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, _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 @@ def __ror__(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)
 
@@ -992,6 +1113,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.
 
@@ -1022,18 +1147,20 @@ 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) 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 0000000000000..539fdeccd14b3
--- /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 756a7ce474aee..4cc82ffcdf39a 100644
--- a/Objects/genericaliasobject.c
+++ b/Objects/genericaliasobject.c
@@ -156,13 +156,24 @@ ga_repr(PyObject *self)
     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);



More information about the Python-checkins mailing list