Skip to content

Commit

Permalink
gh-118285: Fix signatures of operator.{attrgetter,itemgetter,methodca…
Browse files Browse the repository at this point in the history
…ller} 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.
  • Loading branch information
serhiy-storchaka authored Apr 29, 2024
1 parent 51c70de commit 444ac0b
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 5 deletions.
7 changes: 7 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions Lib/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_operator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import inspect
import pickle
import sys
from decimal import Decimal
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions Modules/_operator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down

0 comments on commit 444ac0b

Please sign in to comment.