[Python-checkins] bpo-43783: Add ParamSpecArgs/Kwargs (GH-25298)

gvanrossum webhook-mailer at python.org
Sat Apr 10 22:57:12 EDT 2021


https://github.com/python/cpython/commit/522433601a5c64603dab3d733f41a5db39d237eb
commit: 522433601a5c64603dab3d733f41a5db39d237eb
branch: master
author: Jelle Zijlstra <jelle.zijlstra at gmail.com>
committer: gvanrossum <gvanrossum at gmail.com>
date: 2021-04-10T19:57:05-07:00
summary:

bpo-43783: Add ParamSpecArgs/Kwargs (GH-25298)

files:
A Misc/NEWS.d/next/Library/2021-04-08-19-32-26.bpo-47383.YI1hdL.rst
M Doc/library/typing.rst
M Doc/whatsnew/3.10.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 8af57f34a6d25..c0c6cdde221b0 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 09064ec38e944..50c8d53e57d83 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 3b8efe16c6e23..7183686a6dcde 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -25,7 +25,7 @@
 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 @@ def __iand__(self, other: Const["MySet[T]"]) -> "MySet[T]":
 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 C(Generic[T]): pass
         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 @@ def test_valid_uses(self):
         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 6224930c3b027..6461ba23dd7e2 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -114,6 +114,8 @@
     'no_type_check_decorator',
     'NoReturn',
     'overload',
+    'ParamSpecArgs',
+    'ParamSpecKwargs',
     'runtime_checkable',
     'Text',
     'TYPE_CHECKING',
@@ -727,6 +729,44 @@ def __init__(self, name, *constraints, bound=None,
             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 @@ def add_two(x: float, y: float) -> float:
     __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 0000000000000..8b680065ea7c3
--- /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.



More information about the Python-checkins mailing list