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

This commit is contained in:
kj 2020-12-24 12:33:48 +08:00 committed by GitHub
parent cc3467a57b
commit 73607be686
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 381 additions and 75 deletions

View file

@ -416,7 +416,7 @@ class Collection(Sized, Iterable, Container):
class _CallableGenericAlias(GenericAlias): class _CallableGenericAlias(GenericAlias):
""" Represent `Callable[argtypes, resulttype]`. """ 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``. followed by ``resulttype``.
Example: ``Callable[[int, str], float]`` sets ``__args__`` to Example: ``Callable[[int, str], float]`` sets ``__args__`` to
@ -444,7 +444,7 @@ class _CallableGenericAlias(GenericAlias):
return super().__new__(cls, origin, ga_args) return super().__new__(cls, origin, ga_args)
def __repr__(self): def __repr__(self):
if len(self.__args__) == 2 and self.__args__[0] is Ellipsis: if _has_special_args(self.__args__):
return super().__repr__() return super().__repr__()
return (f'collections.abc.Callable' return (f'collections.abc.Callable'
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
@ -452,7 +452,7 @@ class _CallableGenericAlias(GenericAlias):
def __reduce__(self): def __reduce__(self):
args = self.__args__ 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] args = list(args[:-1]), args[-1]
return _CallableGenericAlias, (Callable, args) return _CallableGenericAlias, (Callable, args)
@ -461,12 +461,28 @@ class _CallableGenericAlias(GenericAlias):
# rather than the default types.GenericAlias object. # rather than the default types.GenericAlias object.
ga = super().__getitem__(item) ga = super().__getitem__(item)
args = ga.__args__ args = ga.__args__
t_result = args[-1] # args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
t_args = args[:-1] if not isinstance(ga.__args__[0], tuple):
args = (t_args, t_result) t_result = ga.__args__[-1]
t_args = ga.__args__[:-1]
args = (t_args, t_result)
return _CallableGenericAlias(Callable, args) 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): def _type_repr(obj):
"""Return the repr() of an object, special-casing types (internal helper). """Return the repr() of an object, special-casing types (internal helper).

View file

@ -369,6 +369,27 @@ class BaseTest(unittest.TestCase):
self.assertEqual(c1.__args__, c2.__args__) self.assertEqual(c1.__args__, c2.__args__)
self.assertEqual(hash(c1.__args__), hash(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__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -25,6 +25,7 @@ from typing import IO, TextIO, BinaryIO
from typing import Pattern, Match from typing import Pattern, Match
from typing import Annotated, ForwardRef from typing import Annotated, ForwardRef
from typing import TypeAlias from typing import TypeAlias
from typing import ParamSpec, Concatenate
import abc import abc
import typing import typing
import weakref import weakref
@ -1130,10 +1131,6 @@ class ProtocolTests(BaseTestCase):
PR[int] PR[int]
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
P[int, str] P[int, str]
with self.assertRaises(TypeError):
PR[int, 1]
with self.assertRaises(TypeError):
PR[int, ClassVar]
class C(PR[int, T]): pass class C(PR[int, T]): pass
@ -1155,8 +1152,6 @@ class ProtocolTests(BaseTestCase):
self.assertIsSubclass(P, PR) self.assertIsSubclass(P, PR)
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
PR[int] PR[int]
with self.assertRaises(TypeError):
PR[int, 1]
class P1(Protocol, Generic[T]): class P1(Protocol, Generic[T]):
def bar(self, x: T) -> str: ... def bar(self, x: T) -> str: ...
@ -1175,8 +1170,6 @@ class ProtocolTests(BaseTestCase):
return x return x
self.assertIsInstance(Test(), PSub) self.assertIsInstance(Test(), PSub)
with self.assertRaises(TypeError):
PR[int, ClassVar]
def test_init_called(self): def test_init_called(self):
T = TypeVar('T') T = TypeVar('T')
@ -1746,8 +1739,6 @@ class GenericTests(BaseTestCase):
self.assertEqual(typing.Iterable[Tuple[T, T]][T], typing.Iterable[Tuple[T, T]]) self.assertEqual(typing.Iterable[Tuple[T, T]][T], typing.Iterable[Tuple[T, T]])
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
Tuple[T, int][()] Tuple[T, int][()]
with self.assertRaises(TypeError):
Tuple[T, U][T, ...]
self.assertEqual(Union[T, int][int], int) self.assertEqual(Union[T, int][int], int)
self.assertEqual(Union[T, U][int, Union[int, str]], Union[int, str]) 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[[T], T][KT], Callable[[KT], KT])
self.assertEqual(Callable[..., List[T]][int], Callable[..., List[int]]) 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): def test_extended_generic_rules_repr(self):
T = TypeVar('T') T = TypeVar('T')
@ -4243,6 +4230,111 @@ class TypeAliasTests(BaseTestCase):
TypeAlias[int] 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): class AllTests(BaseTestCase):
"""Tests for __all__.""" """Tests for __all__."""

View file

@ -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: At large scale, the structure of the module is following:
* Imports and exports, all public names should be explicitly added to __all__. * 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. * Internal helper functions: these should never be used in code outside this module.
* _SpecialForm and its instances (special forms): Any, NoReturn, ClassVar, Union, Optional * _SpecialForm and its instances (special forms):
* Two classes whose instances can be type arguments in addition to types: ForwardRef and TypeVar 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 * 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], currently only used by Tuple and Callable. All subscripted types like X[int], Union[int, str],
etc., are instances of either of these classes. etc., are instances of either of these classes.
@ -36,11 +38,13 @@ __all__ = [
'Any', 'Any',
'Callable', 'Callable',
'ClassVar', 'ClassVar',
'Concatenate',
'Final', 'Final',
'ForwardRef', 'ForwardRef',
'Generic', 'Generic',
'Literal', 'Literal',
'Optional', 'Optional',
'ParamSpec',
'Protocol', 'Protocol',
'Tuple', 'Tuple',
'Type', 'Type',
@ -154,7 +158,7 @@ def _type_check(arg, msg, is_argument=True):
return arg return arg
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol): if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
raise TypeError(f"Plain {arg} is not valid as type argument") 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 return arg
if not callable(arg): if not callable(arg):
raise TypeError(f"{msg} Got {arg!r:.100}.") raise TypeError(f"{msg} Got {arg!r:.100}.")
@ -183,14 +187,14 @@ def _type_repr(obj):
def _collect_type_vars(types): def _collect_type_vars(types):
"""Collect all type variable contained in types in order of """Collect all type variable-like variables contained
first appearance (lexicographic order). For example:: in types in order of first appearance (lexicographic order). For example::
_collect_type_vars((T, List[S, T])) == (T, S) _collect_type_vars((T, List[S, T])) == (T, S)
""" """
tvars = [] tvars = []
for t in types: 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) tvars.append(t)
if isinstance(t, (_GenericAlias, GenericAlias)): if isinstance(t, (_GenericAlias, GenericAlias)):
tvars.extend([t for t in t.__parameters__ if t not in tvars]) 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};" raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
f" actual {alen}, expected {elen}") 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): def _deduplicate(params):
# Weed out strict duplicates, preserving the first of each occurrence. # 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") 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): class ForwardRef(_Final, _root=True):
"""Internal wrapper to hold a forward reference.""" """Internal wrapper to hold a forward reference."""
@ -585,8 +627,41 @@ class ForwardRef(_Final, _root=True):
def __repr__(self): def __repr__(self):
return f'ForwardRef({self.__forward_arg__!r})' 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. """Type variable.
Usage:: Usage::
@ -636,20 +711,13 @@ class TypeVar(_Final, _Immutable, _root=True):
def __init__(self, name, *constraints, bound=None, def __init__(self, name, *constraints, bound=None,
covariant=False, contravariant=False): covariant=False, contravariant=False):
self.__name__ = name self.__name__ = name
if covariant and contravariant: super().__init__(bound, covariant, contravariant)
raise ValueError("Bivariant types are not supported.")
self.__covariant__ = bool(covariant)
self.__contravariant__ = bool(contravariant)
if constraints and bound is not None: if constraints and bound is not None:
raise TypeError("Constraints cannot be combined with bound=...") raise TypeError("Constraints cannot be combined with bound=...")
if constraints and len(constraints) == 1: if constraints and len(constraints) == 1:
raise TypeError("A single constraint is not allowed") raise TypeError("A single constraint is not allowed")
msg = "TypeVar(name, constraint, ...): constraints must be types." msg = "TypeVar(name, constraint, ...): constraints must be types."
self.__constraints__ = tuple(_type_check(t, msg) for t in constraints) 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: try:
def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') # for pickling def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') # for pickling
except (AttributeError, ValueError): except (AttributeError, ValueError):
@ -657,23 +725,68 @@ class TypeVar(_Final, _Immutable, _root=True):
if def_mod != 'typing': if def_mod != 'typing':
self.__module__ = def_mod self.__module__ = def_mod
def __or__(self, right):
return Union[self, right]
def __ror__(self, right): class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True):
return Union[self, right] """Parameter specification variable.
def __repr__(self): Usage::
if self.__covariant__:
prefix = '+'
elif self.__contravariant__:
prefix = '-'
else:
prefix = '~'
return prefix + self.__name__
def __reduce__(self): P = ParamSpec('P')
return self.__name__
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): def _is_dunder(attr):
@ -783,21 +896,26 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
raise TypeError(f"Cannot subscript already-subscripted {self}") raise TypeError(f"Cannot subscript already-subscripted {self}")
if not isinstance(params, tuple): if not isinstance(params, tuple):
params = (params,) params = (params,)
msg = "Parameters to generic types must be types." params = tuple(_type_convert(p) for p in params)
params = tuple(_type_check(p, msg) 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__)) _check_generic(self, params, len(self.__parameters__))
subst = dict(zip(self.__parameters__, params)) subst = dict(zip(self.__parameters__, params))
new_args = [] new_args = []
for arg in self.__args__: for arg in self.__args__:
if isinstance(arg, TypeVar): if isinstance(arg, _TypeVarLike):
arg = subst[arg] arg = subst[arg]
elif isinstance(arg, (_GenericAlias, GenericAlias)): elif isinstance(arg, (_GenericAlias, GenericAlias)):
subparams = arg.__parameters__ subparams = arg.__parameters__
if subparams: if subparams:
subargs = tuple(subst[x] for x in subparams) subargs = tuple(subst[x] for x in subparams)
arg = arg[subargs] 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)) return self.copy_with(tuple(new_args))
def copy_with(self, params): def copy_with(self, params):
@ -884,15 +1002,18 @@ class _SpecialGenericAlias(_BaseGenericAlias, _root=True):
class _CallableGenericAlias(_GenericAlias, _root=True): class _CallableGenericAlias(_GenericAlias, _root=True):
def __repr__(self): def __repr__(self):
assert self._name == 'Callable' 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 super().__repr__()
return (f'typing.Callable' return (f'typing.Callable'
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' f'[[{", ".join([_type_repr(a) for a in args[:-1]])}], '
f'{_type_repr(self.__args__[-1])}]') f'{_type_repr(args[-1])}]')
def __reduce__(self): def __reduce__(self):
args = self.__args__ 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] args = list(args[:-1]), args[-1]
return operator.getitem, (Callable, args) return operator.getitem, (Callable, args)
@ -992,6 +1113,10 @@ class _LiteralGenericAlias(_GenericAlias, _root=True):
return hash(frozenset(_value_and_type_iter(self.__args__))) return hash(frozenset(_value_and_type_iter(self.__args__)))
class _ConcatenateGenericAlias(_GenericAlias, _root=True):
pass
class Generic: class Generic:
"""Abstract base class for generic types. """Abstract base class for generic types.
@ -1022,18 +1147,20 @@ class Generic:
if not params and cls is not Tuple: if not params and cls is not Tuple:
raise TypeError( raise TypeError(
f"Parameter list to {cls.__qualname__}[...] cannot be empty") f"Parameter list to {cls.__qualname__}[...] cannot be empty")
msg = "Parameters to generic types must be types." params = tuple(_type_convert(p) for p in params)
params = tuple(_type_check(p, msg) for p in params)
if cls in (Generic, Protocol): if cls in (Generic, Protocol):
# Generic and Protocol can only be subscripted with unique type variables. # 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( 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): if len(set(params)) != len(params):
raise TypeError( raise TypeError(
f"Parameters to {cls.__name__}[...] must all be unique") f"Parameters to {cls.__name__}[...] must all be unique")
else: else:
# Subscripting a regular Generic subclass. # 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__)) _check_generic(cls, params, len(cls.__parameters__))
return _GenericAlias(cls, params) return _GenericAlias(cls, params)

View file

@ -0,0 +1,2 @@
Implemented :pep:`612`: added ``ParamSpec`` and ``Concatenate`` to
:mod:`typing`. Patch by Ken Jin.

View file

@ -156,13 +156,24 @@ error:
return NULL; return NULL;
} }
// isinstance(obj, TypeVar) without importing typing.py. /* Checks if a variable number of names are from typing.py.
// Returns -1 for errors. * If any one of the names are found, return 1, else 0.
static int **/
is_typevar(PyObject *obj) static inline int
is_typing_name(PyObject *obj, int num, ...)
{ {
va_list names;
va_start(names, num);
PyTypeObject *type = Py_TYPE(obj); 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; return 0;
} }
PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__"); PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__");
@ -172,9 +183,25 @@ is_typevar(PyObject *obj)
int res = PyUnicode_Check(module) int res = PyUnicode_Check(module)
&& _PyUnicode_EqualToASCIIString(module, "typing"); && _PyUnicode_EqualToASCIIString(module, "typing");
Py_DECREF(module); Py_DECREF(module);
va_end(names);
return res; 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) // Index of item in self[:len], or -1 if not found (self is a tuple)
static Py_ssize_t static Py_ssize_t
tuple_index(PyObject *self, Py_ssize_t len, PyObject *item) tuple_index(PyObject *self, Py_ssize_t len, PyObject *item)
@ -209,7 +236,7 @@ make_parameters(PyObject *args)
Py_ssize_t iparam = 0; Py_ssize_t iparam = 0;
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) { for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *t = PyTuple_GET_ITEM(args, iarg); PyObject *t = PyTuple_GET_ITEM(args, iarg);
int typevar = is_typevar(t); int typevar = is_typevarlike(t);
if (typevar < 0) { if (typevar < 0) {
Py_DECREF(parameters); Py_DECREF(parameters);
return NULL; return NULL;
@ -279,7 +306,14 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems)
if (iparam >= 0) { if (iparam >= 0) {
arg = argitems[iparam]; 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); PyTuple_SET_ITEM(subargs, i, arg);
} }
@ -314,11 +348,19 @@ ga_getitem(PyObject *self, PyObject *item)
int is_tuple = PyTuple_Check(item); int is_tuple = PyTuple_Check(item);
Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1; Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1;
PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item; PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item;
if (nitems != nparams) { // A special case in PEP 612 where if X = Callable[P, int],
return PyErr_Format(PyExc_TypeError, // then X[int, str] == X[[int, str]].
"Too %s arguments for %R", if (nparams == 1 && nitems > 1 && is_tuple &&
nitems > nparams ? "many" : "few", is_paramspec(PyTuple_GET_ITEM(alias->parameters, 0))) {
self); 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) /* Replace all type variables (specified by alias->parameters)
with corresponding values specified by argitems. 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++) { for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg); PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg);
int typevar = is_typevar(arg); int typevar = is_typevarlike(arg);
if (typevar < 0) { if (typevar < 0) {
Py_DECREF(newargs); Py_DECREF(newargs);
return NULL; return NULL;
@ -342,7 +384,13 @@ ga_getitem(PyObject *self, PyObject *item)
Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg); Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg);
assert(iparam >= 0); assert(iparam >= 0);
arg = argitems[iparam]; 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 { else {
arg = subs_tvars(arg, alias->parameters, argitems); arg = subs_tvars(arg, alias->parameters, argitems);