8000 bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing by Fidget-Spinner · Pull Request #23702 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Dec 24, 2020
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
219b4ee
Add ParamSpec and Concatenate
Fidget-Spinner Dec 8, 2020
a1c0d0a
support ParamSpec in generics
Fidget-Spinner Dec 8, 2020
7b3beab
Add typing tests, disallow Concatenate in other types
Fidget-Spinner Dec 9, 2020
5dd3b44
Add news
Fidget-Spinner Dec 9, 2020
59c0b20
Address some of Guido's review comments
Fidget-Spinner Dec 10, 2020
4c381b3
remove extraneous empty lines
Fidget-Spinner Dec 10, 2020
b36b62d
Support ParamSpec in __parameters__ of typing and builtin GenericAlias
Fidget-Spinner Dec 10, 2020
d09d088
add tests for user defined generics
Fidget-Spinner Dec 10, 2020
0a19f34
Merge remote-tracking branch 'upstream/master' into pep612
Fidget-Spinner Dec 14, 2020
9727e2a
cast list to tuple done, loosened type checks for Generic
Fidget-Spinner Dec 14, 2020
cc7fc1c
loosen generics, allow typevar-like subst, flatten out args if Callable
Fidget-Spinner Dec 15, 2020
3e67f23
fix whitespace issue, cast list to tuple for types.GenericAlias
Fidget-Spinner Dec 15, 2020
d9baa1a
convert list to tuples if substituting paramspecs in types.GenericAlias
Fidget-Spinner Dec 16, 2020
c4155b6
done! flattened __args__ in substitutions for collections.abc.Callable
Fidget-Spinner Dec 16, 2020
2dbf861
fix repr problems, add repr tests
Fidget-Spinner Dec 16, 2020
87c2d19
Add another test for multiple chaining
Fidget-Spinner Dec 16, 2020
2b09de6
fix typo
Fidget-Spinner Dec 16, 2020
d980702
Clean up some comments
Fidget-Spinner Dec 17, 2020
45f7894
Merge remote-tracking branch 'upstream/master' into pep612
Fidget-Spinner Dec 22, 2020
d6f777c
remove stray whitespace
Fidget-Spinner Dec 22, 2020
9a8176b
Address nearly all of Guido's reviews
Fidget-Spinner Dec 23, 2020
fa06838
more reviews; fix some docstrings, clean up code, cast list to tuple …
Fidget-Spinner Dec 23, 2020
b8672cd
remove uneeded tests copied over from typevar
Fidget-Spinner Dec 23, 2020
51a463c
remove unused variable
Fidget-Spinner Dec 23, 2020
6d5b754
Merge remote-tracking branch 'upstream/master' into pep612
Fidget-Spinner Dec 24, 2020
c05d5d7
merge length checking into _has_special_args too
Fidget-Spinner Dec 24, 2020
c49ba30
Update Lib/_collections_abc.py
gvanrossum Dec 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add ParamSpec and Concatenate
  • Loading branch information
Fidget-Spinner committed Dec 8, 2020
commit 219b4ee5f98f5293e75024103f2f9d8400f577e2
166 changes: 134 additions & 32 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
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
* Three 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.
Expand Down Expand Up @@ -36,11 +36,13 @@
'Any',
'Callable',
'ClassVar',
'Concatenate',
'Final',
'ForwardRef',
'Generic',
'Literal',
'Optional',
'ParamSpec',
'Protocol',
'Tuple',
'Type',
Expand Down Expand Up @@ -147,7 +149,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}.")
Expand Down Expand Up @@ -516,6 +518,22 @@ 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 add, removes or transform parameters of a Callable.
"""
if parameters == ():
raise TypeError("Cannot take a Concatenate of no types.")
8000 if not isinstance(parameters, tuple):
parameters = (parameters,)
msg = "Concatenate[arg, ...]: each arg must be a type."
parameters = tuple(_type_check(p, msg) for p in parameters)
if not any(isinstance(p, ParamSpec) for p in parameters):
raise TypeError("Concatenate must contain at least one ParamSpec variable.")
return _ConcatenateGenericAlias(self, parameters)


class ForwardRef(_Final, _root=True):
"""Internal wrapper to hold a forward reference."""

Expand Down Expand Up @@ -578,8 +596,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 _setup_bound_cov_contra(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::
Expand Down Expand Up @@ -629,44 +680,84 @@ 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)
self._setup_bound_cov_contra(bound=bound, covariant=covariant,
contravariant=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):
def_mod = 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 serve primarily 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 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 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
self._setup_bound_cov_contra(bound=bound, covariant=covariant,
contravariant=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):
Expand Down Expand Up @@ -874,18 +965,22 @@ def __or__(self, right):
def __ror__(self, right):
return Union[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)

Expand All @@ -903,10 +998,11 @@ def __getitem__(self, params):
if args is Ellipsis:
params = (Ellipsis, result)
else:
if not isinstance(args, list):
if not isinstance(args, (list, ParamSpec, _ConcatenateGenericAlias)):
raise TypeError(f"Callable[args, result]: args must be a list."
f" Got {args}")
params = (tuple(args), result)
if isinstance(args, list):
params = (tuple(args), result)
return self.__getitem_inner__(params)

@_tp_cache
Expand All @@ -916,6 +1012,8 @@ def __getitem_inner__(self, params):
result = _type_check(result, msg)
if args is Ellipsis:
return self.copy_with((_TypingEllipsis, result))
elif isinstance(args, (ParamSpec, _ConcatenateGenericAlias)):
return self.copy_with((args, result))
msg = "Callable[[arg, ...], result]: each arg must be a type."
args = tuple(_type_check(arg, msg) for arg in args)
params = args + (result,)
Expand Down Expand Up @@ -984,6 +1082,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.

Expand Down
0