mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702)
This commit is contained in:
parent
cc3467a57b
commit
73607be686
6 changed files with 381 additions and 75 deletions
209
Lib/typing.py
209
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)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue