Skip to content

Commit bf17d41

Browse files
jdemeyermiss-islington
authored andcommitted
bpo-37645: add new function _PyObject_FunctionStr() (GH-14890)
Additional note: the `method_check_args` function in `Objects/descrobject.c` is written in such a way that it applies to all kinds of descriptors. In particular, a future re-implementation of `wrapper_descriptor` could use that code. CC @vstinner @encukou https://bugs.python.org/issue37645 Automerge-Triggered-By: @encukou
1 parent b396663 commit bf17d41

File tree

11 files changed

+171
-94
lines changed

11 files changed

+171
-94
lines changed

Diff for: Doc/c-api/object.rst

+1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ Object Protocol
196196
This function now includes a debug assertion to help ensure that it
197197
does not silently discard an active exception.
198198
199+
199200
.. c:function:: PyObject* PyObject_Bytes(PyObject *o)
200201
201202
.. index:: builtin: bytes

Diff for: Include/cpython/object.h

+1
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ static inline void _Py_Dealloc_inline(PyObject *op)
348348
}
349349
#define _Py_Dealloc(op) _Py_Dealloc_inline(op)
350350

351+
PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
351352

352353
/* Safely decref `op` and set `op` to `op2`.
353354
*

Diff for: Lib/test/test_call.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_varargs3_kw(self):
7474
self.assertRaisesRegex(TypeError, msg, bool, x=2)
7575

7676
def test_varargs4_kw(self):
77-
msg = r"^index\(\) takes no keyword arguments$"
77+
msg = r"^list[.]index\(\) takes no keyword arguments$"
7878
self.assertRaisesRegex(TypeError, msg, [].index, x=2)
7979

8080
def test_varargs5_kw(self):
@@ -90,19 +90,19 @@ def test_varargs7_kw(self):
9090
self.assertRaisesRegex(TypeError, msg, next, x=2)
9191

9292
def test_varargs8_kw(self):
93-
msg = r"^pack\(\) takes no keyword arguments$"
93+
msg = r"^_struct[.]pack\(\) takes no keyword arguments$"
9494
self.assertRaisesRegex(TypeError, msg, struct.pack, x=2)
9595

9696
def test_varargs9_kw(self):
97-
msg = r"^pack_into\(\) takes no keyword arguments$"
97+
msg = r"^_struct[.]pack_into\(\) takes no keyword arguments$"
9898
self.assertRaisesRegex(TypeError, msg, struct.pack_into, x=2)
9999

100100
def test_varargs10_kw(self):
101-
msg = r"^index\(\) takes no keyword arguments$"
101+
msg = r"^deque[.]index\(\) takes no keyword arguments$"
102102
self.assertRaisesRegex(TypeError, msg, collections.deque().index, x=2)
103103

104104
def test_varargs11_kw(self):
105-
msg = r"^pack\(\) takes no keyword arguments$"
105+
msg = r"^Struct[.]pack\(\) takes no keyword arguments$"
106106
self.assertRaisesRegex(TypeError, msg, struct.Struct.pack, struct.Struct(""), x=2)
107107

108108
def test_varargs12_kw(self):

Diff for: Lib/test/test_descr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1967,7 +1967,7 @@ def test_methods_in_c(self):
19671967
# different error messages.
19681968
set_add = set.add
19691969

1970-
expected_errmsg = "descriptor 'add' of 'set' object needs an argument"
1970+
expected_errmsg = "unbound method set.add() needs an argument"
19711971

19721972
with self.assertRaises(TypeError) as cm:
19731973
set_add()

Diff for: Lib/test/test_extcall.py

+19-19
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@
5252
>>> f(1, 2, **{'a': -1, 'b': 5}, **{'a': 4, 'c': 6})
5353
Traceback (most recent call last):
5454
...
55-
TypeError: f() got multiple values for keyword argument 'a'
55+
TypeError: test.test_extcall.f() got multiple values for keyword argument 'a'
5656
>>> f(1, 2, **{'a': -1, 'b': 5}, a=4, c=6)
5757
Traceback (most recent call last):
5858
...
59-
TypeError: f() got multiple values for keyword argument 'a'
59+
TypeError: test.test_extcall.f() got multiple values for keyword argument 'a'
6060
>>> f(1, 2, a=3, **{'a': 4}, **{'a': 5})
6161
Traceback (most recent call last):
6262
...
63-
TypeError: f() got multiple values for keyword argument 'a'
63+
TypeError: test.test_extcall.f() got multiple values for keyword argument 'a'
6464
>>> f(1, 2, 3, *[4, 5], **{'a':6, 'b':7})
6565
(1, 2, 3, 4, 5) {'a': 6, 'b': 7}
6666
>>> f(1, 2, 3, x=4, y=5, *(6, 7), **{'a':8, 'b': 9})
@@ -118,7 +118,7 @@
118118
>>> g(*Nothing())
119119
Traceback (most recent call last):
120120
...
121-
TypeError: g() argument after * must be an iterable, not Nothing
121+
TypeError: test.test_extcall.g() argument after * must be an iterable, not Nothing
122122
123123
>>> class Nothing:
124124
... def __len__(self): return 5
@@ -127,7 +127,7 @@
127127
>>> g(*Nothing())
128128
Traceback (most recent call last):
129129
...
130-
TypeError: g() argument after * must be an iterable, not Nothing
130+
TypeError: test.test_extcall.g() argument after * must be an iterable, not Nothing
131131
132132
>>> class Nothing():
133133
... def __len__(self): return 5
@@ -247,17 +247,17 @@
247247
>>> h(*h)
248248
Traceback (most recent call last):
249249
...
250-
TypeError: h() argument after * must be an iterable, not function
250+
TypeError: test.test_extcall.h() argument after * must be an iterable, not function
251251
252252
>>> h(1, *h)
253253
Traceback (most recent call last):
254254
...
255-
TypeError: h() argument after * must be an iterable, not function
255+
TypeError: test.test_extcall.h() argument after * must be an iterable, not function
256256
257257
>>> h(*[1], *h)
258258
Traceback (most recent call last):
259259
...
260-
TypeError: h() argument after * must be an iterable, not function
260+
TypeError: test.test_extcall.h() argument after * must be an iterable, not function
261261
262262
>>> dir(*h)
263263
Traceback (most recent call last):
@@ -268,38 +268,38 @@
268268
>>> nothing(*h)
269269
Traceback (most recent call last):
270270
...
271-
TypeError: NoneType object argument after * must be an iterable, \
271+
TypeError: None argument after * must be an iterable, \
272272
not function
273273
274274
>>> h(**h)
275275
Traceback (most recent call last):
276276
...
277-
TypeError: h() argument after ** must be a mapping, not function
277+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not function
278278
279279
>>> h(**[])
280280
Traceback (most recent call last):
281281
...
282-
TypeError: h() argument after ** must be a mapping, not list
282+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not list
283283
284284
>>> h(a=1, **h)
285285
Traceback (most recent call last):
286286
...
287-
TypeError: h() argument after ** must be a mapping, not function
287+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not function
288288
289289
>>> h(a=1, **[])
290290
Traceback (most recent call last):
291291
...
292-
TypeError: h() argument after ** must be a mapping, not list
292+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not list
293293
294294
>>> h(**{'a': 1}, **h)
295295
Traceback (most recent call last):
296296
...
297-
TypeError: h() argument after ** must be a mapping, not function
297+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not function
298298
299299
>>> h(**{'a': 1}, **[])
300300
Traceback (most recent call last):
301301
...
302-
TypeError: h() argument after ** must be a mapping, not list
302+
TypeError: test.test_extcall.h() argument after ** must be a mapping, not list
303303
304304
>>> dir(**h)
305305
Traceback (most recent call last):
@@ -309,7 +309,7 @@
309309
>>> nothing(**h)
310310
Traceback (most recent call last):
311311
...
312-
TypeError: NoneType object argument after ** must be a mapping, \
312+
TypeError: None argument after ** must be a mapping, \
313313
not function
314314
315315
>>> dir(b=1, **{'b': 1})
@@ -351,17 +351,17 @@
351351
>>> g(**MultiDict([('x', 1), ('x', 2)]))
352352
Traceback (most recent call last):
353353
...
354-
TypeError: g() got multiple values for keyword argument 'x'
354+
TypeError: test.test_extcall.g() got multiple values for keyword argument 'x'
355355
356356
>>> g(a=3, **MultiDict([('x', 1), ('x', 2)]))
357357
Traceback (most recent call last):
358358
...
359-
TypeError: g() got multiple values for keyword argument 'x'
359+
TypeError: test.test_extcall.g() got multiple values for keyword argument 'x'
360360
361361
>>> g(**MultiDict([('a', 3)]), **MultiDict([('x', 1), ('x', 2)]))
362362
Traceback (most recent call last):
363363
...
364-
TypeError: g() got multiple values for keyword argument 'x'
364+
TypeError: test.test_extcall.g() got multiple values for keyword argument 'x'
365365
366366
Another helper function
367367

Diff for: Lib/test/test_unpack_ex.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -236,27 +236,27 @@
236236
>>> f(x=5, **{'x': 3}, y=2)
237237
Traceback (most recent call last):
238238
...
239-
TypeError: f() got multiple values for keyword argument 'x'
239+
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
240240
241241
>>> f(**{'x': 3}, x=5, y=2)
242242
Traceback (most recent call last):
243243
...
244-
TypeError: f() got multiple values for keyword argument 'x'
244+
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
245245
246246
>>> f(**{'x': 3}, **{'x': 5}, y=2)
247247
Traceback (most recent call last):
248248
...
249-
TypeError: f() got multiple values for keyword argument 'x'
249+
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
250250
251251
>>> f(x=5, **{'x': 3}, **{'x': 2})
252252
Traceback (most recent call last):
253253
...
254-
TypeError: f() got multiple values for keyword argument 'x'
254+
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
255255
256256
>>> f(**{1: 3}, **{1: 5})
257257
Traceback (most recent call last):
258258
...
259-
TypeError: f() got multiple values for keyword argument '1'
259+
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument '1'
260260
261261
Unpacking non-sequence
262262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`_PyObject_FunctionStr` to get a user-friendly string representation
2+
of a function-like object. Patch by Jeroen Demeyer.

Diff for: Objects/descrobject.c

+29-28
Original file line numberDiff line numberDiff line change
@@ -231,45 +231,38 @@ getset_set(PyGetSetDescrObject *descr, PyObject *obj, PyObject *value)
231231
*
232232
* First, common helpers
233233
*/
234-
static const char *
235-
get_name(PyObject *func) {
236-
assert(PyObject_TypeCheck(func, &PyMethodDescr_Type));
237-
return ((PyMethodDescrObject *)func)->d_method->ml_name;
238-
}
239-
240-
typedef void (*funcptr)(void);
241-
242234
static inline int
243235
method_check_args(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
244236
{
245237
assert(!PyErr_Occurred());
246-
assert(PyObject_TypeCheck(func, &PyMethodDescr_Type));
247238
if (nargs < 1) {
248-
PyErr_Format(PyExc_TypeError,
249-
"descriptor '%.200s' of '%.100s' "
250-
"object needs an argument",
251-
get_name(func), PyDescr_TYPE(func)->tp_name);
239+
PyObject *funcstr = _PyObject_FunctionStr(func);
240+
if (funcstr != NULL) {
241+
PyErr_Format(PyExc_TypeError,
242+
"unbound method %U needs an argument", funcstr);
243+
Py_DECREF(funcstr);
244+
}
252245
return -1;
253246
}
254247
PyObject *self = args[0];
255-
if (!_PyObject_RealIsSubclass((PyObject *)Py_TYPE(self),
256-
(PyObject *)PyDescr_TYPE(func)))
257-
{
258-
PyErr_Format(PyExc_TypeError,
259-
"descriptor '%.200s' for '%.100s' objects "
260-
"doesn't apply to a '%.100s' object",
261-
get_name(func), PyDescr_TYPE(func)->tp_name,
262-
Py_TYPE(self)->tp_name);
248+
PyObject *dummy;
249+
if (descr_check((PyDescrObject *)func, self, &dummy)) {
263250
return -1;
264251
}
265252
if (kwnames && PyTuple_GET_SIZE(kwnames)) {
266-
PyErr_Format(PyExc_TypeError,
267-
"%.200s() takes no keyword arguments", get_name(func));
253+
PyObject *funcstr = _PyObject_FunctionStr(func);
254+
if (funcstr != NULL) {
255+
PyErr_Format(PyExc_TypeError,
256+
"%U takes no keyword arguments", funcstr);
257+
Py_DECREF(funcstr);
258+
}
268259
return -1;
269260
}
270261
return 0;
271262
}
272263

264+
typedef void (*funcptr)(void);
265+
273266
static inline funcptr
274267
method_enter_call(PyThreadState *tstate, PyObject *func)
275268
{
@@ -387,8 +380,12 @@ method_vectorcall_NOARGS(
387380
return NULL;
388381
}
389382
if (nargs != 1) {
390-
PyErr_Format(PyExc_TypeError,
391-
"%.200s() takes no arguments (%zd given)", get_name(func), nargs-1);
383+
PyObject *funcstr = _PyObject_FunctionStr(func);
384+
if (funcstr != NULL) {
385+
PyErr_Format(PyExc_TypeError,
386+
"%U takes no arguments (%zd given)", funcstr, nargs-1);
387+
Py_DECREF(funcstr);
388+
}
392389
return NULL;
393390
}
394391
PyCFunction meth = (PyCFunction)method_enter_call(tstate, func);
@@ -410,9 +407,13 @@ method_vectorcall_O(
410407
return NULL;
411408
}
412409
if (nargs != 2) {
413-
PyErr_Format(PyExc_TypeError,
414-
"%.200s() takes exactly one argument (%zd given)",
415-
get_name(func), nargs-1);
410+
PyObject *funcstr = _PyObject_FunctionStr(func);
411+
if (funcstr != NULL) {
412+
PyErr_Format(PyExc_TypeError,
413+
"%U takes exactly one argument (%zd given)",
414+
funcstr, nargs-1);
415+
Py_DECREF(funcstr);
416+
}
416417
return NULL;
417418
}
418419
PyCFunction meth = (PyCFunction)method_enter_call(tstate, func);

Diff for: Objects/methodobject.c

+20-17
Original file line numberDiff line numberDiff line change
@@ -334,29 +334,26 @@ _PyCFunction_Fini(void)
334334
*
335335
* First, common helpers
336336
*/
337-
static const char *
338-
get_name(PyObject *func)
339-
{
340-
assert(PyCFunction_Check(func));
341-
PyMethodDef *method = ((PyCFunctionObject *)func)->m_ml;
342-
return method->ml_name;
343-
}
344-
345-
typedef void (*funcptr)(void);
346337

347338
static inline int
348339
cfunction_check_kwargs(PyThreadState *tstate, PyObject *func, PyObject *kwnames)
349340
{
350341
assert(!_PyErr_Occurred(tstate));
351342
assert(PyCFunction_Check(func));
352343
if (kwnames && PyTuple_GET_SIZE(kwnames)) {
353-
_PyErr_Format(tstate, PyExc_TypeError,
354-
"%.200s() takes no keyword arguments", get_name(func));
344+
PyObject *funcstr = _PyObject_FunctionStr(func);
345+
if (funcstr != NULL) {
346+
_PyErr_Format(tstate, PyExc_TypeError,
347+
"%U takes no keyword arguments", funcstr);
348+
Py_DECREF(funcstr);
349+
}
355350
return -1;
356351
}
357352
return 0;
358353
}
359354

355+
typedef void (*funcptr)(void);
356+
360357
static inline funcptr
361358
cfunction_enter_call(PyThreadState *tstate, PyObject *func)
362359
{
@@ -412,9 +409,12 @@ cfunction_vectorcall_NOARGS(
412409
}
413410
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
414411
if (nargs != 0) {
415-
_PyErr_Format(tstate, PyExc_TypeError,
416-
"%.200s() takes no arguments (%zd given)",
417-
get_name(func), nargs);
412+
PyObject *funcstr = _PyObject_FunctionStr(func);
413+
if (funcstr != NULL) {
414+
_PyErr_Format(tstate, PyExc_TypeError,
415+
"%U takes no arguments (%zd given)", funcstr, nargs);
416+
Py_DECREF(funcstr);
417+
}
418418
return NULL;
419419
}
420420
PyCFunction meth = (PyCFunction)cfunction_enter_call(tstate, func);
@@ -436,9 +436,12 @@ cfunction_vectorcall_O(
436436
}
437437
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
438438
if (nargs != 1) {
439-
_PyErr_Format(tstate, PyExc_TypeError,
440-
"%.200s() takes exactly one argument (%zd given)",
441-
get_name(func), nargs);
439+
PyObject *funcstr = _PyObject_FunctionStr(func);
440+
if (funcstr != NULL) {
441+
_PyErr_Format(tstate, PyExc_TypeError,
442+
"%U takes exactly one argument (%zd given)", funcstr, nargs);
443+
Py_DECREF(funcstr);
444+
}
442445
return NULL;
443446
}
444447
PyCFunction meth = (PyCFunction)cfunction_enter_call(tstate, func);

0 commit comments

Comments
 (0)