https://github.com/python/cpython/commit/444ac0b7a64ff6b6caba9c2731bd33151ce18ad1
commit: 444ac0b7a64ff6b6caba9c2731bd33151ce18ad1
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2024-04-29T19:30:48+03:00
summary:

gh-118285: Fix signatures of operator.{attrgetter,itemgetter,methodcaller} 
instances (GH-118316)

* 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.

files:
A Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst
M Lib/inspect.py
M Lib/operator.py
M Lib/test/test_inspect/test_inspect.py
M Lib/test/test_operator.py
M Modules/_operator.c

diff --git a/Lib/inspect.py b/Lib/inspect.py
index 3c346b27b1f06d..1f4216f0389d28 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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)
 
diff --git a/Lib/operator.py b/Lib/operator.py
index 30116c1189a499..02ccdaa13ddb31 100644
--- a/Lib/operator.py
+++ b/Lib/operator.py
@@ -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 @@ def func(obj):
                 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 @@ def func(obj):
                 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 @@ def __init__(self, name, /, *args, **kwargs):
         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):
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index 169d1edb706fc3..6b577090bdff68 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -4090,6 +4090,28 @@ class C:
                             ((('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):
diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py
index 0d34d671563d19..f8eac8dc002636 100644
--- a/Lib/test/test_operator.py
+++ b/Lib/test/test_operator.py
@@ -1,4 +1,5 @@
 import unittest
+import inspect
 import pickle
 import sys
 from decimal import Decimal
@@ -602,6 +603,28 @@ def test_dunder_is_original(self):
             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
index 00000000000000..6e8f8d368ca5a6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst
@@ -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.
diff --git a/Modules/_operator.c b/Modules/_operator.c
index 1f6496d381adac..306d4508f52a68 100644
--- a/Modules/_operator.c
+++ b/Modules/_operator.c
@@ -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},

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to