diff options
author | kj <28750310+Fidget-Spinner@users.noreply.github.com> | 2020-12-24 04:33:48 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-24 04:33:48 (GMT) |
commit | 73607be68668ab7f4bee53507c8dc7b5a46c9cb4 (patch) | |
tree | 4ec6bb3f178e61b7fc61d979ec65519a4ef1d4bc /Lib/typing.py | |
parent | cc3467a57b61b0e7ef254b36790a1c44b13f2228 (diff) | |
download | cpython-73607be68668ab7f4bee53507c8dc7b5a46c9cb4.zip cpython-73607be68668ab7f4bee53507c8dc7b5a46c9cb4.tar.gz cpython-73607be68668ab7f4bee53507c8dc7b5a46c9cb4.tar.bz2 |
bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702)
Diffstat (limited to 'Lib/typing.py')
-rw-r--r-- | Lib/typing.py | 209 |
1 files changed, 168 insertions, 41 deletions
diff --git a/Lib/typing.py b/Lib/typing.py index 7f07321..7b79876 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 + + 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 @@ 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) |