summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-04-29 16:30:48 (GMT)
committerGitHub <noreply@github.com>2024-04-29 16:30:48 (GMT)
commit444ac0b7a64ff6b6caba9c2731bd33151ce18ad1 (patch)
tree572fd300a05e5a177da821c2afc371e3d2122f4a
parent51c70de998ead35674bf4b2b236e9ce8e89d17b4 (diff)
downloadcpython-444ac0b7a64ff6b6caba9c2731bd33151ce18ad1.zip
cpython-444ac0b7a64ff6b6caba9c2731bd33151ce18ad1.tar.gz
cpython-444ac0b7a64ff6b6caba9c2731bd33151ce18ad1.tar.bz2
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.
-rw-r--r--Lib/inspect.py7
-rw-r--r--Lib/operator.py10
-rw-r--r--Lib/test/test_inspect/test_inspect.py22
-rw-r--r--Lib/test/test_operator.py23
-rw-r--r--Misc/NEWS.d/next/Library/2024-04-26-14-53-28.gh-issue-118285.A0_pte.rst4
-rw-r--r--Modules/_operator.c15
6 files changed, 76 insertions, 5 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 3c346b2..1f4216f 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 30116c1..02ccdaa 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 @@ 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):
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index 169d1edb..6b57709 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -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):
diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py
index 0d34d67..f8eac8d 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 @@ 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
index 0000000..6e8f8d3
--- /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 1f6496d..306d450 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},