diff options
author | Matthew Rahtz <matthew.rahtz@gmail.com> | 2022-03-08 04:02:55 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-08 04:02:55 (GMT) |
commit | 7a793a388b017be635ea41ef75b0fd8bcf75a309 (patch) | |
tree | a9f3ba378f268ebbab6d97d1407b8b84862f5ccc /Lib/typing.py | |
parent | 13331a12c3c4d5007e2ab61514ba1d76cedd8b84 (diff) | |
download | cpython-7a793a388b017be635ea41ef75b0fd8bcf75a309.zip cpython-7a793a388b017be635ea41ef75b0fd8bcf75a309.tar.gz cpython-7a793a388b017be635ea41ef75b0fd8bcf75a309.tar.bz2 |
bpo-43224: Implement PEP 646 changes to typing.py (GH-31021)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Diffstat (limited to 'Lib/typing.py')
-rw-r--r-- | Lib/typing.py | 235 |
1 files changed, 214 insertions, 21 deletions
diff --git a/Lib/typing.py b/Lib/typing.py index 721afb7..abb8bce 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -5,7 +5,7 @@ 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, Never, ClassVar, Union, Optional, Concatenate + Any, NoReturn, Never, ClassVar, Union, Optional, Concatenate, Unpack * 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 @@ -56,6 +56,7 @@ __all__ = [ 'Tuple', 'Type', 'TypeVar', + 'TypeVarTuple', 'Union', # ABCs (from collections.abc). @@ -139,6 +140,7 @@ __all__ = [ 'TYPE_CHECKING', 'TypeAlias', 'TypeGuard', + 'Unpack', ] # The pseudo-submodules 're' and 'io' are part of the public @@ -182,7 +184,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms= 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.UnionType, ParamSpec, - ParamSpecArgs, ParamSpecKwargs)): + ParamSpecArgs, ParamSpecKwargs, TypeVarTuple)): return arg if not callable(arg): raise TypeError(f"{msg} Got {arg!r:.100}.") @@ -793,8 +795,28 @@ class ForwardRef(_Final, _root=True): module_repr = f', module={self.__forward_module__!r}' return f'ForwardRef({self.__forward_arg__!r}{module_repr})' -class _TypeVarLike: - """Mixin for TypeVar-like types (TypeVar and ParamSpec).""" + +def _is_unpacked_typevartuple(x: Any) -> bool: + return ( + isinstance(x, _UnpackGenericAlias) + # If x is Unpack[tuple[...]], __parameters__ will be empty. + and x.__parameters__ + and isinstance(x.__parameters__[0], TypeVarTuple) + ) + + +def _is_typevar_like(x: Any) -> bool: + return isinstance(x, (TypeVar, ParamSpec)) or _is_unpacked_typevartuple(x) + + +class _BoundVarianceMixin: + """Mixin giving __init__ bound and variance arguments. + + This is used by TypeVar and ParamSpec, which both employ the notions of + a type 'bound' (restricting type arguments to be a subtype of some + specified type) and type 'variance' (determining subtype relations between + generic types). + """ def __init__(self, bound, covariant, contravariant): """Used to setup TypeVars and ParamSpec's bound, covariant and contravariant attributes. @@ -827,7 +849,7 @@ class _TypeVarLike: return self.__name__ -class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): +class TypeVar(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Type variable. Usage:: @@ -886,6 +908,39 @@ class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): self.__module__ = def_mod +class TypeVarTuple(_Final, _Immutable, _root=True): + """Type variable tuple. + + Usage: + + Ts = TypeVarTuple('Ts') # Can be given any name + + Just as a TypeVar (type variable) is a placeholder for a single type, + a TypeVarTuple is a placeholder for an *arbitrary* number of types. For + example, if we define a generic class using a TypeVarTuple: + + class C(Generic[*Ts]): ... + + Then we can parameterize that class with an arbitrary number of type + arguments: + + C[int] # Fine + C[int, str] # Also fine + C[()] # Even this is fine + + For more details, see PEP 646. + """ + + def __init__(self, name): + self._name = name + + def __iter__(self): + yield Unpack[self] + + def __repr__(self): + return self._name + + class ParamSpecArgs(_Final, _Immutable, _root=True): """The args for a ParamSpec object. @@ -934,7 +989,7 @@ class ParamSpecKwargs(_Final, _Immutable, _root=True): return self.__origin__ == other.__origin__ -class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): +class ParamSpec(_Final, _Immutable, _BoundVarianceMixin, _root=True): """Parameter specification variable. Usage:: @@ -1065,6 +1120,45 @@ class _BaseGenericAlias(_Final, _root=True): return list(set(super().__dir__() + [attr for attr in dir(self.__origin__) if not _is_dunder(attr)])) + +def _is_unpacked_tuple(x: Any) -> bool: + # Is `x` something like `*tuple[int]` or `*tuple[int, ...]`? + if not isinstance(x, _UnpackGenericAlias): + return False + # Alright, `x` is `Unpack[something]`. + + # `x` will always have `__args__`, because Unpack[] and Unpack[()] + # aren't legal. + unpacked_type = x.__args__[0] + + return getattr(unpacked_type, '__origin__', None) is tuple + + +def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: + if not _is_unpacked_tuple(x): + return False + unpacked_tuple = x.__args__[0] + + if not hasattr(unpacked_tuple, '__args__'): + # It's `Unpack[tuple]`. We can't make any assumptions about the length + # of the tuple, so it's effectively an arbitrary-length tuple. + return True + + tuple_args = unpacked_tuple.__args__ + if not tuple_args: + # It's `Unpack[tuple[()]]`. + return False + + last_arg = tuple_args[-1] + if last_arg is Ellipsis: + # It's `Unpack[tuple[something, ...]]`, which is arbitrary-length. + return True + + # If the arguments didn't end with an ellipsis, then it's not an + # arbitrary-length tuple. + return False + + # Special typing constructs Union, Optional, Generic, Callable and Tuple # use three special attributes for internal bookkeeping of generic types: # * __parameters__ is a tuple of unique free type parameters of a generic @@ -1103,7 +1197,7 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # TypeVar[bool] def __init__(self, origin, args, *, inst=True, name=None, - _typevar_types=TypeVar, + _typevar_types=(TypeVar, TypeVarTuple), _paramspec_tvars=False): super().__init__(origin, inst=inst, name=name) if not isinstance(args, tuple): @@ -1160,7 +1254,10 @@ class _GenericAlias(_BaseGenericAlias, _root=True): if (self._paramspec_tvars and any(isinstance(t, ParamSpec) for t in self.__parameters__)): args = _prepare_paramspec_params(self, args) - else: + elif not any(isinstance(p, TypeVarTuple) for p in self.__parameters__): + # We only run this if there are no TypeVarTuples, because we + # don't check variadic generic arity at runtime (to reduce + # complexity of typing.py). _check_generic(self, args, len(self.__parameters__)) new_args = self._determine_new_args(args) @@ -1182,6 +1279,10 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # anything more exotic than a plain `TypeVar`, we need to consider # edge cases. + if any(isinstance(p, TypeVarTuple) for p in self.__parameters__): + raise NotImplementedError( + "Type substitution for TypeVarTuples is not yet implemented" + ) # In the example above, this would be {T3: str} new_arg_by_param = dict(zip(self.__parameters__, args)) @@ -1195,6 +1296,10 @@ class _GenericAlias(_BaseGenericAlias, _root=True): f"ParamSpec, or Concatenate. Got {new_arg}") elif isinstance(old_arg, self._typevar_types): new_arg = new_arg_by_param[old_arg] + elif (TypeVarTuple in self._typevar_types + and _is_unpacked_typevartuple(old_arg)): + original_typevartuple = old_arg.__parameters__[0] + new_arg = new_arg_by_param[original_typevartuple] elif isinstance(old_arg, (_GenericAlias, GenericAlias, types.UnionType)): subparams = old_arg.__parameters__ if not subparams: @@ -1217,6 +1322,17 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # ...we need to be careful; `new_args` should end up as # `(int, str, float)` rather than `([int, str], float)`. new_args.extend(new_arg) + elif _is_unpacked_typevartuple(old_arg): + # Consider the following `_GenericAlias`, `B`: + # class A(Generic[*Ts]): ... + # B = A[T, *Ts] + # If we then do: + # B[float, int, str] + # The `new_arg` corresponding to `T` will be `float`, and the + # `new_arg` corresponding to `*Ts` will be `(int, str)`. We + # should join all these types together in a flat list + # `(float, int, str)` - so again, we should `extend`. + new_args.extend(new_arg) else: new_args.append(new_arg) @@ -1230,7 +1346,11 @@ class _GenericAlias(_BaseGenericAlias, _root=True): name = 'typing.' + self._name else: name = _type_repr(self.__origin__) - args = ", ".join([_type_repr(a) for a in self.__args__]) + if self.__args__: + args = ", ".join([_type_repr(a) for a in self.__args__]) + else: + # To ensure the repr is eval-able. + args = "()" return f'{name}[{args}]' def __reduce__(self): @@ -1258,6 +1378,9 @@ class _GenericAlias(_BaseGenericAlias, _root=True): return () return (self.__origin__,) + def __iter__(self): + yield Unpack[self] + # _nparams is the number of accepted parameters, e.g. 0 for Hashable, # 1 for List and 2 for Dict. It may be -1 if variable number of @@ -1365,10 +1488,10 @@ class _TupleType(_SpecialGenericAlias, _root=True): return self.copy_with((_TypingEmpty,)) if not isinstance(params, tuple): params = (params,) - if len(params) == 2 and params[1] is ...: + if len(params) >= 2 and params[-1] is ...: msg = "Tuple[t, ...]: t must be a type." - p = _type_check(params[0], msg) - return self.copy_with((p, _TypingEllipsis)) + params = tuple(_type_check(p, msg) for p in params[:-1]) + return self.copy_with((*params, _TypingEllipsis)) msg = "Tuple[t0, t1, ...]: each t must be a type." params = tuple(_type_check(p, msg) for p in params) return self.copy_with(params) @@ -1441,6 +1564,48 @@ class _ConcatenateGenericAlias(_GenericAlias, _root=True): return super().copy_with(params) +@_SpecialForm +def Unpack(self, parameters): + """Type unpack operator. + + The type unpack operator takes the child types from some container type, + such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For + example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + + From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + + Note that there is only some runtime checking of this operator. Not + everything the runtime allows may be accepted by static type checkers. + + For more information, see PEP 646. + """ + item = _type_check(parameters, f'{self} accepts only single type.') + return _UnpackGenericAlias(origin=self, args=(item,)) + + +class _UnpackGenericAlias(_GenericAlias, _root=True): + + def __repr__(self): + # `Unpack` only takes one argument, so __args__ should contain only + # a single item. + return '*' + repr(self.__args__[0]) + + class Generic: """Abstract base class for generic types. @@ -1466,15 +1631,36 @@ class Generic: @_tp_cache def __class_getitem__(cls, params): + """Parameterizes a generic class. + + At least, parameterizing a generic class is the *main* thing this method + does. For example, for some generic class `Foo`, this is called when we + do `Foo[int]` - there, with `cls=Foo` and `params=int`. + + However, note that this method is also called when defining generic + classes in the first place with `class Foo(Generic[T]): ...`. + """ if not isinstance(params, tuple): params = (params,) - if not params and cls is not Tuple: - raise TypeError( - f"Parameter list to {cls.__qualname__}[...] cannot be empty") + + if not params: + # We're only ok with `params` being empty if the class's only type + # parameter is a `TypeVarTuple` (which can contain zero types). + class_params = getattr(cls, "__parameters__", None) + only_class_parameter_is_typevartuple = ( + class_params is not None + and len(class_params) == 1 + and isinstance(class_params[0], TypeVarTuple) + ) + if not only_class_parameter_is_typevartuple: + raise TypeError( + f"Parameter list to {cls.__qualname__}[...] cannot be empty" + ) + 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, ParamSpec)) for p in params): + if not all(_is_typevar_like(p) for p in params): raise TypeError( f"Parameters to {cls.__name__}[...] must all be type variables " f"or parameter specification variables.") @@ -1485,11 +1671,16 @@ class Generic: # Subscripting a regular Generic subclass. if any(isinstance(t, ParamSpec) for t in cls.__parameters__): params = _prepare_paramspec_params(cls, params) - else: + elif not any(isinstance(p, TypeVarTuple) for p in cls.__parameters__): + # We only run this if there are no TypeVarTuples, because we + # don't check variadic generic arity at runtime (to reduce + # complexity of typing.py). _check_generic(cls, params, len(cls.__parameters__)) - return _GenericAlias(cls, params, - _typevar_types=(TypeVar, ParamSpec), - _paramspec_tvars=True) + return _GenericAlias( + cls, params, + _typevar_types=(TypeVar, TypeVarTuple, ParamSpec), + _paramspec_tvars=True, + ) def __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(*args, **kwargs) @@ -1501,7 +1692,9 @@ class Generic: if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: - tvars = _collect_type_vars(cls.__orig_bases__, (TypeVar, ParamSpec)) + tvars = _collect_type_vars( + cls.__orig_bases__, (TypeVar, TypeVarTuple, ParamSpec) + ) # Look for Generic[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. |