From 522433601a5c64603dab3d733f41a5db39d237eb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 10 Apr 2021 19:57:05 -0700 Subject: [PATCH] bpo-43783: Add ParamSpecArgs/Kwargs (GH-25298) --- Doc/library/typing.rst | 24 ++++++++- Doc/whatsnew/3.10.rst | 6 ++- Lib/test/test_typing.py | 16 ++++-- Lib/typing.py | 53 +++++++++++++++++-- .../2021-04-08-19-32-26.bpo-47383.YI1hdL.rst | 3 ++ 5 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-04-08-19-32-26.bpo-47383.YI1hdL.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 8af57f34a6d..c0c6cdde221 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1058,8 +1058,10 @@ These are not used in annotations. They are building blocks for creating generic components. ``P.args`` represents the tuple of positional parameters in a given call and should only be used to annotate ``*args``. ``P.kwargs`` represents the mapping of keyword parameters to their values in a given call, - and should be only be used to annotate ``**kwargs`` or ``**kwds``. Both - attributes require the annotated parameter to be in scope. + and should be only be used to annotate ``**kwargs``. Both + attributes require the annotated parameter to be in scope. At runtime, + ``P.args`` and ``P.kwargs`` are instances respectively of + :class:`ParamSpecArgs` and :class:`ParamSpecKwargs`. Parameter specification variables created with ``covariant=True`` or ``contravariant=True`` can be used to declare covariant or contravariant @@ -1078,6 +1080,24 @@ These are not used in annotations. They are building blocks for creating generic ``ParamSpec`` and ``Concatenate``). * :class:`Callable` and :class:`Concatenate`. +.. data:: ParamSpecArgs +.. data:: ParamSpecKwargs + + Arguments and keyword arguments attributes of a :class:`ParamSpec`. The + ``P.args`` attribute of a ``ParamSpec`` is an instance of ``ParamSpecArgs``, + and ``P.kwargs`` is an instance of ``ParamSpecKwargs``. They are intended + for runtime introspection and have no special meaning to static type checkers. + + Calling :func:`get_origin` on either of these objects will return the + original ``ParamSpec``:: + + P = ParamSpec("P") + get_origin(P.args) # returns P + get_origin(P.kwargs) # returns P + + .. versionadded:: 3.10 + + .. data:: AnyStr ``AnyStr`` is a type variable defined as diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 09064ec38e9..50c8d53e57d 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -550,9 +550,11 @@ which adds or removes parameters of another callable. Examples of usage can be found in :class:`typing.Concatenate`. See :class:`typing.Callable`, :class:`typing.ParamSpec`, -:class:`typing.Concatenate` and :pep:`612` for more details. +:class:`typing.Concatenate`, :class:`typing.ParamSpecArgs`, +:class:`typing.ParamSpecKwargs`, and :pep:`612` for more details. -(Contributed by Ken Jin in :issue:`41559`.) +(Contributed by Ken Jin in :issue:`41559`, with minor enhancements by Jelle +Zijlstra in :issue:`43783`. PEP written by Mark Mendoza.) PEP 613: TypeAlias Annotation diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3b8efe16c6e..7183686a6dc 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -25,7 +25,7 @@ from typing import IO, TextIO, BinaryIO from typing import Pattern, Match from typing import Annotated, ForwardRef from typing import TypeAlias -from typing import ParamSpec, Concatenate +from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs import abc import typing import weakref @@ -3004,6 +3004,7 @@ class GetTypeHintTests(BaseTestCase): class GetUtilitiesTestCase(TestCase): def test_get_origin(self): T = TypeVar('T') + P = ParamSpec('P') class C(Generic[T]): pass self.assertIs(get_origin(C[int]), C) self.assertIs(get_origin(C[T]), C) @@ -3022,6 +3023,8 @@ class GetUtilitiesTestCase(TestCase): self.assertIs(get_origin(list[int]), list) self.assertIs(get_origin(list), None) self.assertIs(get_origin(list | str), types.Union) + self.assertIs(get_origin(P.args), P) + self.assertIs(get_origin(P.kwargs), P) def test_get_args(self): T = TypeVar('T') @@ -4265,11 +4268,16 @@ class ParamSpecTests(BaseTestCase): self.assertEqual(C4.__args__, (P, T)) self.assertEqual(C4.__parameters__, (P, T)) - # ParamSpec instances should also have args and kwargs attributes. + def test_args_kwargs(self): + P = ParamSpec('P') self.assertIn('args', dir(P)) self.assertIn('kwargs', dir(P)) - P.args - P.kwargs + self.assertIsInstance(P.args, ParamSpecArgs) + self.assertIsInstance(P.kwargs, ParamSpecKwargs) + self.assertIs(P.args.__origin__, P) + self.assertIs(P.kwargs.__origin__, P) + self.assertEqual(repr(P.args), "P.args") + self.assertEqual(repr(P.kwargs), "P.kwargs") def test_user_generics(self): T = TypeVar("T") diff --git a/Lib/typing.py b/Lib/typing.py index 6224930c3b0..6461ba23dd7 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -114,6 +114,8 @@ __all__ = [ 'no_type_check_decorator', 'NoReturn', 'overload', + 'ParamSpecArgs', + 'ParamSpecKwargs', 'runtime_checkable', 'Text', 'TYPE_CHECKING', @@ -727,6 +729,44 @@ class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True): self.__module__ = def_mod +class ParamSpecArgs(_Final, _Immutable, _root=True): + """The args for a ParamSpec object. + + Given a ParamSpec object P, P.args is an instance of ParamSpecArgs. + + ParamSpecArgs objects have a reference back to their ParamSpec: + + P.args.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.args" + + +class ParamSpecKwargs(_Final, _Immutable, _root=True): + """The kwargs for a ParamSpec object. + + Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs. + + ParamSpecKwargs objects have a reference back to their ParamSpec: + + P.kwargs.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.kwargs" + + class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): """Parameter specification variable. @@ -776,8 +816,13 @@ class ParamSpec(_Final, _Immutable, _TypeVarLike, _root=True): __slots__ = ('__name__', '__bound__', '__covariant__', '__contravariant__', '__dict__') - args = object() - kwargs = object() + @property + def args(self): + return ParamSpecArgs(self) + + @property + def kwargs(self): + return ParamSpecKwargs(self) def __init__(self, name, *, bound=None, covariant=False, contravariant=False): self.__name__ = name @@ -1662,10 +1707,12 @@ def get_origin(tp): get_origin(Generic[T]) is Generic get_origin(Union[T, int]) is Union get_origin(List[Tuple[T, T]][int]) == list + get_origin(P.args) is P """ if isinstance(tp, _AnnotatedAlias): return Annotated - if isinstance(tp, (_BaseGenericAlias, GenericAlias)): + if isinstance(tp, (_BaseGenericAlias, GenericAlias, + ParamSpecArgs, ParamSpecKwargs)): return tp.__origin__ if tp is Generic: return Generic diff --git a/Misc/NEWS.d/next/Library/2021-04-08-19-32-26.bpo-47383.YI1hdL.rst b/Misc/NEWS.d/next/Library/2021-04-08-19-32-26.bpo-47383.YI1hdL.rst new file mode 100644 index 00000000000..8b680065ea7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-08-19-32-26.bpo-47383.YI1hdL.rst @@ -0,0 +1,3 @@ +The ``P.args`` and ``P.kwargs`` attributes of :class:`typing.ParamSpec` are +now instances of the new classes :class:`typing.ParamSpecArgs` and +:class:`typing.ParamSpecKwargs`, which enables a more useful ``repr()``. Patch by Jelle Zijlstra.