10000 bpo-37645: add new function _PyObject_FunctionStr() by jdemeyer · Pull Request #14890 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-37645: add new function _PyObject_FunctionStr() #14890

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 5, 2019
11 changes: 11 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ Object Protocol
This function now includes a debug assertion to help ensure that it
does not silently discard an active exception.


.. c:function:: PyObject* _PyObject_FunctionStr(PyObject *func)

Return a user-friendly string representation of the function-like object
*func*. This returns ``func.__qualname__ + "()"`` if there is a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning __qualname__ without module is not particularly useful.

If you want to return a full qualified name (long but unambiguous), return __module__ + '.' + __qualname__. __module__ can be omitted if it is 'builtins'.

If you want to return a short name (it is enough in many times), return __name__.

if __qualname__ is not available, you can use __name__ instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if __qualname__ is not available, you can use __name__ instead.

This is mainly meant as replacement for PyEval_GetFuncName and all classes supported by that function implement __qualname__. So I see little reason for the additional complexity of supporting __name__.

``__qualname__`` attribute and ``str(func)`` otherwise.
Note that there is no check that *func* is actually callable.

.. versionadded:: 3.9


.. c:function:: PyObject* PyObject_Bytes(PyObject *o)

.. index:: builtin: bytes
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ static inline void _Py_Dealloc_inline(PyObject *op)
}
#define _Py_Dealloc(op) _Py_Dealloc_inline(op)

PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);

/* Safely decref `op` and set `op` to `op2`.
*
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def test_varargs3_kw(self):
self.assertRaisesRegex(TypeError, msg, bool, x=2)

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

def test_varargs5_kw(self):
Expand All @@ -217,11 +217,11 @@ def test_varargs9_kw(self):
self.assertRaisesRegex(TypeError, msg, struct.pack_into, x=2)

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

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

def test_varargs12_kw(self):
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1967,7 +1967,7 @@ def test_methods_in_c(self):
# different error messages.
set_add = set.add

expected_errmsg = "descriptor 'add' of 'set' object needs an argument"
expected_errmsg = "set.add() needs an argument"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider this a regression: the message is now too similar to calling the method on an instance, but missing an argument.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: set.add() takes exactly one argument (0 given)

>>> set.add()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: set.add() needs an argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change the error message to anything you like, except that it must contain the string set.add (the function name). So it could be

TypeError: unbound method set.add() needs an argument

or whatever (surely, this is better than anything mentioning descriptors).


Small rant: the bug is really this:

>>> set().add()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add() takes exactly one argument (0 given)

For a Python method, it would correctly note that 2 arguments are required. This is difficult to fix in CPython since builtin_function_or_method doesn't really know whether it's a function or method. This was one of the things that the rejected PEP 580 would have addressed.


with self.assertRaises(TypeError) as cm:
set_add()
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_extcall.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
>>> nothing(*h)
Traceback (most recent call last):
...
TypeError: NoneType object argument after * must be an iterable, \
TypeError: None argument after * must be an iterable, \
not function

>>> h(**h)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`_PyObject_FunctionStr` to get a user-friendly string representation
of a function-like object.
57 changes: 29 additions & 28 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,45 +231,38 @@ getset_set(PyGetSetDescrObject *descr, PyObject *obj, PyObject *value)
*
* First, common helpers
*/
static const char *
get_name(PyObject *func) {
assert(PyObject_TypeCheck(func, &PyMethodDescr_Type));
return ((PyMethodDescrObject *)func)->d_method->ml_name;
}

typedef void (*funcptr)(void);

static inline int
method_check_args(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
assert(!PyErr_Occurred());
assert(PyObject_TypeCheck(func, &PyMethodDescr_Type));
if (nargs < 1) {
PyErr_Format(PyExc_TypeError,
"descriptor '%.200s' of '%.100s' "
"object needs an argument",
get_name(func), PyDescr_TYPE(func)->tp_name);
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U needs an argument", funcstr);
Py_DECREF(funcstr);
}
return -1;
}
PyObject *self = args[0];
if (!_PyObject_RealIsSubclass((PyObject *)Py_TYPE(self),
(PyObject *)PyDescr_TYPE(func)))
{
PyErr_Format(PyExc_TypeError,
"descriptor '%.200s' for '%.100s' objects "
"doesn't apply to a '%.100s' object",
get_name(func), PyDescr_TYPE(func)->tp_name,
Py_TYPE(self)->tp_name);
PyObject *dummy;
if (descr_check((PyDescrObject *)func, self, &dummy)) {
return -1;
}
if (kwnames && PyTuple_GET_SIZE(kwnames)) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes no keyword arguments", get_name(func));
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes no keyword arguments", funcstr);
Py_DECREF(funcstr);
}
return -1;
}
return 0;
}

typedef void (*funcptr)(void);

static inline funcptr
method_enter_call(PyObject *func)
{
Expand Down Expand Up @@ -382,8 +375,12 @@ method_vectorcall_NOARGS(
return NULL;
}
if (nargs != 1) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes no arguments (%zd given)", get_name(func), nargs-1);
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes no arguments (%zd given)", funcstr, nargs-1);
Py_DECREF(funcstr);
}
return NULL;
}
PyCFunction meth = (PyCFunction)method_enter_call(func);
Expand All @@ -404,9 +401,13 @@ method_vectorcall_O(
return NULL;
}
if (nargs != 2) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes exactly one argument (%zd given)",
get_name(func), nargs-1);
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes exactly one argument (%zd given)",
funcstr, nargs-1);
Py_DECREF(funcstr);
}
return NULL;
}
PyCFunction meth = (PyCFunction)method_enter_call(func);
Expand Down
36 changes: 20 additions & 16 deletions Objects/methodobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -331,29 +331,26 @@ PyCFunction_Fini(void)
*
* First, common helpers
*/
static const char *
get_name(PyObject *func)
{
assert(PyCFunction_Check(func));
PyMethodDef *method = ((PyCFunctionObject *)func)->m_ml;
return method->ml_name;
}

typedef void (*funcptr)(void);

static inline int
cfunction_check_kwargs(PyObject *func, PyObject *kwnames)
{
assert(!PyErr_Occurred());
assert(PyCFunction_Check(func));
if (kwnames && PyTuple_GET_SIZE(kwnames)) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes no keyword arguments", get_name(func));
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes no keyword arguments", funcstr);
Py_DECREF(funcstr);
}
return -1;
}
return 0;
}

typedef void (*funcptr)(void);

static inline funcptr
cfunction_enter_call(PyObject *func)
{
Expand Down Expand Up @@ -406,8 +403,12 @@ cfunction_vectorcall_NOARGS(
}
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
if (nargs != 0) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes no arguments (%zd given)", get_name(func), nargs);
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes no arguments (%zd given)", funcstr, nargs);
Py_DECREF(funcstr);
}
return NULL;
}
PyCFunction meth = (PyCFunction)cfunction_enter_call(func);
Expand All @@ -428,9 +429,12 @@ cfunction_vectorcall_O(
}
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
if (nargs != 1) {
PyErr_Format(PyExc_TypeError,
"%.200s() takes exactly one argument (%zd given)",
get_name(func), nargs);
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
PyErr_Format(PyExc_TypeError,
"%U takes exactly one argument (%zd given)", funcstr, nargs);
Py_DECREF(funcstr);
}
return NULL;
}
PyCFunction meth = (PyCFunction)cfunction_enter_call(func);
Expand Down
26 changes: 26 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,32 @@ PyObject_Bytes(PyObject *v)
return PyBytes_FromObject(v);
}


/*
def _PyObject_FunctionStr(f):
try:
return f.__qualname__ + "()"
except Exception:
return str(f)
*/
PyObject *
_PyObject_FunctionStr(PyObject *f)
{
_Py_IDENTIFIER(__qualname__);
PyObject *name = _PyObject_GetAttrId(f, &PyId___qualname__);
if (name != NULL) {
PyObject *res = PyUnicode_FromFormat("%S()", name);
Py_DECREF(name);
return res;
}
/* __qualname__ lookup failed */
if (!PyErr_ExceptionMatches(PyExc_Exception)) {
return NULL;
}
PyErr_Clear();
return PyObject_Str(f);
}

/* For Python 3.0.1 and later, the old three-way comparison has been
completely removed in favour of rich comparisons. PyObject_Compare() and
PyObject_Cmp() are gone, and the builtin cmp function no longer exists.
Expand Down
16 changes: 10 additions & 6 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -5341,12 +5341,16 @@ static int
check_args_iterable(PyThreadState *tstate, PyObject *func, PyObject *args)
{
if (args->ob_type->tp_iter == NULL && !PySequence_Check(args)) {
_PyErr_Format(tstate, PyExc_TypeError,
"%.200s%.200s argument after * "
"must be an iterable, not %.200s",
PyEval_GetFuncName(func),
PyEval_GetFuncDesc(func),
args->ob_type->tp_name);
/* check_args_iterable() may be called with a live exception,
* clear it. */
PyErr_Clear();
PyObject *funcstr = _PyObject_FunctionStr(func);
if (funcstr != NULL) {
_PyErr_Format(tstate, PyExc_TypeError,
"%U argument after * must be an iterable, not %.200s",
funcstr, Py_TYPE(args)->tp_name);
Py_DECREF(funcstr);
}
return -1;
}
return 0;
Expand Down
0