]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-118285: Fix signatures of operator.{attrgetter,itemgetter,methodcaller} instances...
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 29 Apr 2024 16:30:48 +0000 (19:30 +0300)
committerGitHub <noreply@github.com>
Mon, 29 Apr 2024 16:30:48 +0000 (19:30 +0300)
* Allow to specify the signature of custom callable instances of extension
  type by the __text_signature__ attribute.
* Specify signatures of operator.attrgetter, operator.itemgetter, and
  operator.methodcaller instances.

Lib/inspect.py
Lib/operator.py
Lib/test/test_inspect/test_inspect.py
Lib/test/test_operator.py
Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst [new file with mode: 0644]
Modules/_operator.c

index 3c346b27b1f06dd59d972b21aee3897a39c25e39..1f4216f0389d28faee6416f59823809cbe19221b 100644 (file)
@@ -2692,6 +2692,13 @@ def _signature_from_callable(obj, *,
         # An object with __call__
         call = getattr_static(type(obj), '__call__', None)
         if call is not None:
+            try:
+                text_sig = obj.__text_signature__
+            except AttributeError:
+                pass
+            else:
+                if text_sig:
+                    return _signature_fromstr(sigcls, obj, text_sig)
             call = _descriptor_get(call, obj)
             return _get_signature_of(call)
 
index 30116c1189a49958e97b2b0b3f1a25e131087a5c..02ccdaa13ddb3183381e5114e00cab3051ff9671 100644 (file)
@@ -239,7 +239,7 @@ class attrgetter:
     """
     __slots__ = ('_attrs', '_call')
 
-    def __init__(self, attr, *attrs):
+    def __init__(self, attr, /, *attrs):
         if not attrs:
             if not isinstance(attr, str):
                 raise TypeError('attribute name must be a string')
@@ -257,7 +257,7 @@ class attrgetter:
                 return tuple(getter(obj) for getter in getters)
             self._call = func
 
-    def __call__(self, obj):
+    def __call__(self, obj, /):
         return self._call(obj)
 
     def __repr__(self):
@@ -276,7 +276,7 @@ class itemgetter:
     """
     __slots__ = ('_items', '_call')
 
-    def __init__(self, item, *items):
+    def __init__(self, item, /, *items):
         if not items:
             self._items = (item,)
             def func(obj):
@@ -288,7 +288,7 @@ class itemgetter:
                 return tuple(obj[i] for i in items)
             self._call = func
 
-    def __call__(self, obj):
+    def __call__(self, obj, /):
         return self._call(obj)
 
     def __repr__(self):
@@ -315,7 +315,7 @@ class methodcaller:
         self._args = args
         self._kwargs = kwargs
 
-    def __call__(self, obj):
+    def __call__(self, obj, /):
         return getattr(obj, self._name)(*self._args, **self._kwargs)
 
     def __repr__(self):
index 169d1edb706fc3a28e9ee5d39c6d0c0d77dbcfe8..6b577090bdff6876db2096229ec5f1ae18d28748 100644 (file)
@@ -4090,6 +4090,28 @@ class TestSignatureObject(unittest.TestCase):
                             ((('a', ..., ..., "positional_or_keyword"),),
                             ...))
 
+    def test_signature_on_callable_objects_with_text_signature_attr(self):
+        class C:
+            __text_signature__ = '(a, /, b, c=True)'
+            def __call__(self, *args, **kwargs):
+                pass
+
+        self.assertEqual(self.signature(C), ((), ...))
+        self.assertEqual(self.signature(C()),
+                         ((('a', ..., ..., "positional_only"),
+                           ('b', ..., ..., "positional_or_keyword"),
+                           ('c', True, ..., "positional_or_keyword"),
+                          ),
+                          ...))
+
+        c = C()
+        c.__text_signature__ = '(x, y)'
+        self.assertEqual(self.signature(c),
+                         ((('x', ..., ..., "positional_or_keyword"),
+                           ('y', ..., ..., "positional_or_keyword"),
+                          ),
+                          ...))
+
     def test_signature_on_wrapper(self):
         class Wrapper:
             def __call__(self, b):
index 0d34d671563d19ac33c93d0a73f7d5b8eca30bb4..f8eac8dc0026369f1088dcc2777e34a77e770135 100644 (file)
@@ -1,4 +1,5 @@
 import unittest
+import inspect
 import pickle
 import sys
 from decimal import Decimal
@@ -602,6 +603,28 @@ class OperatorTestCase:
             if dunder:
                 self.assertIs(dunder, orig)
 
+    def test_attrgetter_signature(self):
+        operator = self.module
+        sig = inspect.signature(operator.attrgetter)
+        self.assertEqual(str(sig), '(attr, /, *attrs)')
+        sig = inspect.signature(operator.attrgetter('x', 'z', 'y'))
+        self.assertEqual(str(sig), '(obj, /)')
+
+    def test_itemgetter_signature(self):
+        operator = self.module
+        sig = inspect.signature(operator.itemgetter)
+        self.assertEqual(str(sig), '(item, /, *items)')
+        sig = inspect.signature(operator.itemgetter(2, 3, 5))
+        self.assertEqual(str(sig), '(obj, /)')
+
+    def test_methodcaller_signature(self):
+        operator = self.module
+        sig = inspect.signature(operator.methodcaller)
+        self.assertEqual(str(sig), '(name, /, *args, **kwargs)')
+        sig = inspect.signature(operator.methodcaller('foo', 2, y=3))
+        self.assertEqual(str(sig), '(obj, /)')
+
+
 class PyOperatorTestCase(OperatorTestCase, unittest.TestCase):
     module = py_operator
 
diff --git a/Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst b/Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst
new file mode 100644 (file)
index 0000000..6e8f8d3
--- /dev/null
@@ -0,0 +1,4 @@
+Allow to specify the signature of custom callable instances of extension
+type by the :attr:`__text_signature__` attribute. Specify signatures of
+:class:`operator.attrgetter`, :class:`operator.itemgetter`, and
+:class:`operator.methodcaller` instances.
index 1f6496d381adacdf01c52fccf46fd40c07cbcfa9..306d4508f52a686f73cddbf3a1d22a33b8c113eb 100644 (file)
@@ -966,6 +966,18 @@ static struct PyMethodDef operator_methods[] = {
 
 };
 
+
+static PyObject *
+text_signature(PyObject *self, void *Py_UNUSED(ignored))
+{
+    return PyUnicode_FromString("(obj, /)");
+}
+
+static PyGetSetDef common_getset[] = {
+    {"__text_signature__", text_signature, (setter)NULL},
+    {NULL}
+};
+
 /* itemgetter object **********************************************************/
 
 typedef struct {
@@ -1171,6 +1183,7 @@ static PyType_Slot itemgetter_type_slots[] = {
     {Py_tp_clear, itemgetter_clear},
     {Py_tp_methods, itemgetter_methods},
     {Py_tp_members, itemgetter_members},
+    {Py_tp_getset, common_getset},
     {Py_tp_new, itemgetter_new},
     {Py_tp_getattro, PyObject_GenericGetAttr},
     {Py_tp_repr, itemgetter_repr},
@@ -1528,6 +1541,7 @@ static PyType_Slot attrgetter_type_slots[] = {
     {Py_tp_clear, attrgetter_clear},
     {Py_tp_methods, attrgetter_methods},
     {Py_tp_members, attrgetter_members},
+    {Py_tp_getset, common_getset},
     {Py_tp_new, attrgetter_new},
     {Py_tp_getattro, PyObject_GenericGetAttr},
     {Py_tp_repr, attrgetter_repr},
@@ -1863,6 +1877,7 @@ static PyType_Slot methodcaller_type_slots[] = {
     {Py_tp_clear, methodcaller_clear},
     {Py_tp_methods, methodcaller_methods},
     {Py_tp_members, methodcaller_members},
+    {Py_tp_getset, common_getset},
     {Py_tp_new, methodcaller_new},
     {Py_tp_getattro, PyObject_GenericGetAttr},
     {Py_tp_repr, methodcaller_repr},