From 2a55a9fdb9f7d637dae9781c4d8e7d65f1e75954 Mon Sep 17 00:00:00 2001 From: Jeroen Demeyer Date: Sun, 21 Jul 2019 20:49:30 +0200 Subject: [PATCH 1/8] bpo-37645: add new function PyObject_FunctionStr() --- Doc/c-api/object.rst | 11 ++++ Include/cpython/object.h | 1 + Lib/test/test_call.py | 6 +- Lib/test/test_descr.py | 2 +- .../2019-07-21-21-08-47.bpo-37645.4DcUaI.rst | 2 + Objects/descrobject.c | 57 ++++++++++--------- Objects/methodobject.c | 36 ++++++------ Objects/object.c | 28 +++++++++ Python/ceval.c | 13 +++-- 9 files changed, 102 insertions(+), 54 deletions(-) create mode 100644 Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index fd1e9c65aaba97..d38826fb090168 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -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 + ``__qualname__`` attribute and ``type(func).__name__ + " object"`` + 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 diff --git a/Include/cpython/object.h b/Include/cpython/object.h index fd4e77103f01b8..a9f7afa379ea9e 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -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`. * diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 0bff7ded4670f1..d21576f9be8e88 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -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): @@ -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): diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 0b43549efb838c..b9a99100f7670f 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -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" with self.assertRaises(TypeError) as cm: set_add() diff --git a/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst new file mode 100644 index 00000000000000..ebbe0339817fba --- /dev/null +++ b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyObject_FunctionStr` to get a user-friendly string representation +of a function-like object. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 9e1b281c4603d6..413667dc0807d8 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -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) { @@ -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); @@ -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); diff --git a/Objects/methodobject.c b/Objects/methodobject.c index b9977467ac08d5..458de125ff1c39 100644 --- a/Objects/methodobject.c +++ b/Objects/methodobject.c @@ -331,15 +331,6 @@ 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) @@ -347,13 +338,19 @@ 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) { @@ -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); @@ -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); diff --git a/Objects/object.c b/Objects/object.c index 7f2c23a9ff8c57..e3c62b2a9c563f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -663,6 +663,34 @@ PyObject_Bytes(PyObject *v) return PyBytes_FromObject(v); } + +/* +def PyObject_FunctionStr(f): + try: + return f.__qualname__ + "()" + except Exception: + return type(f).__name__ + " object" +*/ +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)) { + /* An exception not inheriting from Exception, like KeyboardInterrupt. + * Propagate it. */ + return NULL; + } + PyErr_Clear(); + return PyUnicode_FromFormat("%.200s object", Py_TYPE(f)->tp_name); +} + /* 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. diff --git a/Python/ceval.c b/Python/ceval.c index ee03350031d91e..b28804fae61b06 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -5341,12 +5341,13 @@ 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); + 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; From 733158d78dbb465fffa1f20e95ceb805425b84d4 Mon Sep 17 00:00:00 2001 From: Jeroen Demeyer Date: Sun, 21 Jul 2019 22:30:44 +0200 Subject: [PATCH 2/8] bpo-37645: clear exception in check_args_iterable() --- Python/ceval.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Python/ceval.c b/Python/ceval.c index b28804fae61b06..c529ed454c83c8 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -5341,6 +5341,9 @@ static int check_args_iterable(PyThreadState *tstate, PyObject *func, PyObject *args) { if (args->ob_type->tp_iter == NULL && !PySequence_Check(args)) { + /* 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, From 4c0c877889b406d33a863005361afb1bd19149af Mon Sep 17 00:00:00 2001 From: Jeroen Demeyer Date: Sun, 4 Aug 2019 23:37:26 +0200 Subject: [PATCH 3/8] bpo-37645: rename PyObject_FunctionStr() -> _PyObject_FunctionStr() --- Doc/c-api/object.rst | 2 +- Include/cpython/object.h | 2 +- .../next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst | 2 +- Objects/descrobject.c | 8 ++++---- Objects/methodobject.c | 6 +++--- Objects/object.c | 6 ++---- Python/ceval.c | 2 +- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index d38826fb090168..7d92d8ca6da906 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -197,7 +197,7 @@ Object Protocol does not silently discard an active exception. -.. c:function:: PyObject* PyObject_FunctionStr(PyObject *func) +.. 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 diff --git a/Include/cpython/object.h b/Include/cpython/object.h index a9f7afa379ea9e..75e55995b57413 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -348,7 +348,7 @@ static inline void _Py_Dealloc_inline(PyObject *op) } #define _Py_Dealloc(op) _Py_Dealloc_inline(op) -PyAPI_FUNC(PyObject *) PyObject_FunctionStr(PyObject *); +PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *); /* Safely decref `op` and set `op` to `op2`. * diff --git a/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst index ebbe0339817fba..5e20d05ff30c36 100644 --- a/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst +++ b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst @@ -1,2 +1,2 @@ -Add :c:func:`PyObject_FunctionStr` to get a user-friendly string representation +Add :c:func:`_PyObject_FunctionStr` to get a user-friendly string representation of a function-like object. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 413667dc0807d8..f2a0484f85fec1 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -236,7 +236,7 @@ method_check_args(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObj { assert(!PyErr_Occurred()); if (nargs < 1) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U needs an argument", funcstr); @@ -250,7 +250,7 @@ method_check_args(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObj return -1; } if (kwnames && PyTuple_GET_SIZE(kwnames)) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes no keyword arguments", funcstr); @@ -375,7 +375,7 @@ method_vectorcall_NOARGS( return NULL; } if (nargs != 1) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes no arguments (%zd given)", funcstr, nargs-1); @@ -401,7 +401,7 @@ method_vectorcall_O( return NULL; } if (nargs != 2) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes exactly one argument (%zd given)", diff --git a/Objects/methodobject.c b/Objects/methodobject.c index 458de125ff1c39..cb37689b182d50 100644 --- a/Objects/methodobject.c +++ b/Objects/methodobject.c @@ -338,7 +338,7 @@ cfunction_check_kwargs(PyObject *func, PyObject *kwnames) assert(!PyErr_Occurred()); assert(PyCFunction_Check(func)); if (kwnames && PyTuple_GET_SIZE(kwnames)) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes no keyword arguments", funcstr); @@ -403,7 +403,7 @@ cfunction_vectorcall_NOARGS( } Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); if (nargs != 0) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes no arguments (%zd given)", funcstr, nargs); @@ -429,7 +429,7 @@ cfunction_vectorcall_O( } Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); if (nargs != 1) { - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, "%U takes exactly one argument (%zd given)", funcstr, nargs); diff --git a/Objects/object.c b/Objects/object.c index e3c62b2a9c563f..3116f457b5cb4a 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -665,14 +665,14 @@ PyObject_Bytes(PyObject *v) /* -def PyObject_FunctionStr(f): +def _PyObject_FunctionStr(f): try: return f.__qualname__ + "()" except Exception: return type(f).__name__ + " object" */ PyObject * -PyObject_FunctionStr(PyObject *f) +_PyObject_FunctionStr(PyObject *f) { _Py_IDENTIFIER(__qualname__); PyObject *name = _PyObject_GetAttrId(f, &PyId___qualname__); @@ -683,8 +683,6 @@ PyObject_FunctionStr(PyObject *f) } /* __qualname__ lookup failed */ if (!PyErr_ExceptionMatches(PyExc_Exception)) { - /* An exception not inheriting from Exception, like KeyboardInterrupt. - * Propagate it. */ return NULL; } PyErr_Clear(); diff --git a/Python/ceval.c b/Python/ceval.c index c529ed454c83c8..cc93155d323e87 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -5344,7 +5344,7 @@ check_args_iterable(PyThreadState *tstate, PyObject *func, PyObject *args) /* check_args_iterable() may be called with a live exception, * clear it. */ PyErr_Clear(); - PyObject *funcstr = PyObject_FunctionStr(func); + PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { _PyErr_Format(tstate, PyExc_TypeError, "%U argument after * must be an iterable, not %.200s", From 2e1b44ec82c3e7c5c82dce083fda4cdc4219eeeb Mon Sep 17 00:00:00 2001 From: Jeroen Demeyer Date: Sun, 18 Aug 2019 18:19:58 +0200 Subject: [PATCH 4/8] bpo-37645: use str(func) as fallback in _PyObject_FunctionStr() --- Doc/c-api/object.rst | 4 ++-- Lib/test/test_extcall.py | 2 +- Objects/object.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 7d92d8ca6da906..cfa92ad7c68ed3 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -201,8 +201,8 @@ Object Protocol Return a user-friendly string representation of the function-like object *func*. This returns ``func.__qualname__ + "()"`` if there is a - ``__qualname__`` attribute and ``type(func).__name__ + " object"`` - otherwise. Note that there is no check that *func* is actually callable. + ``__qualname__`` attribute and ``str(func)`` otherwise. + Note that there is no check that *func* is actually callable. .. versionadded:: 3.9 diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index d9dcb709f75415..f44e3326509aa8 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -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) diff --git a/Objects/object.c b/Objects/object.c index 3116f457b5cb4a..512384f39c6000 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -669,7 +669,7 @@ def _PyObject_FunctionStr(f): try: return f.__qualname__ + "()" except Exception: - return type(f).__name__ + " object" + return str(f) */ PyObject * _PyObject_FunctionStr(PyObject *f) @@ -686,7 +686,7 @@ _PyObject_FunctionStr(PyObject *f) return NULL; } PyErr_Clear(); - return PyUnicode_FromFormat("%.200s object", Py_TYPE(f)->tp_name); + return PyObject_Str(f); } /* For Python 3.0.1 and later, the old three-way comparison has been From fa2055690b052a0cf15c83722cd420e2e9adb38f Mon Sep 17 00:00:00 2001 From: Jeroen Demeyer Date: Fri, 13 Sep 2019 22:24:25 +0200 Subject: [PATCH 5/8] bpo-37645: review fixes --- Doc/c-api/object.rst | 10 ------- Lib/test/test_call.py | 4 +-- Lib/test/test_descr.py | 2 +- Lib/test/test_extcall.py | 10 +++---- Objects/descrobject.c | 2 +- Objects/object.c | 58 +++++++++++++++++++++++++++++----------- Python/ceval.c | 5 ++-- 7 files changed, 55 insertions(+), 36 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index cfa92ad7c68ed3..14bf12eccfabf0 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -197,16 +197,6 @@ Object Protocol 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 - ``__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 diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index d21576f9be8e88..01329efb43f13a 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -209,11 +209,11 @@ def test_varargs7_kw(self): self.assertRaisesRegex(TypeError, msg, next, x=2) def test_varargs8_kw(self): - msg = r"^pack\(\) takes no keyword arguments$" + msg = r"^_struct[.]pack\(\) takes no keyword arguments$" self.assertRaisesRegex(TypeError, msg, struct.pack, x=2) def test_varargs9_kw(self): - msg = r"^pack_into\(\) takes no keyword arguments$" + msg = r"^_struct[.]pack_into\(\) takes no keyword arguments$" self.assertRaisesRegex(TypeError, msg, struct.pack_into, x=2) def test_varargs10_kw(self): diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index b9a99100f7670f..c2b8c96389c4c3 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1967,7 +1967,7 @@ def test_methods_in_c(self): # different error messages. set_add = set.add - expected_errmsg = "set.add() needs an argument" + expected_errmsg = "unbound method set.add() needs an argument" with self.assertRaises(TypeError) as cm: set_add() diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index f44e3326509aa8..c1fcf623d713ec 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -118,7 +118,7 @@ >>> g(*Nothing()) Traceback (most recent call last): ... - TypeError: g() argument after * must be an iterable, not Nothing + TypeError: test.test_extcall.g() argument after * must be an iterable, not Nothing >>> class Nothing: ... def __len__(self): return 5 @@ -127,7 +127,7 @@ >>> g(*Nothing()) Traceback (most recent call last): ... - TypeError: g() argument after * must be an iterable, not Nothing + TypeError: test.test_extcall.g() argument after * must be an iterable, not Nothing >>> class Nothing(): ... def __len__(self): return 5 @@ -247,17 +247,17 @@ >>> h(*h) Traceback (most recent call last): ... - TypeError: h() argument after * must be an iterable, not function + TypeError: test.test_extcall.h() argument after * must be an iterable, not function >>> h(1, *h) Traceback (most recent call last): ... - TypeError: h() argument after * must be an iterable, not function + TypeError: test.test_extcall.h() argument after * must be an iterable, not function >>> h(*[1], *h) Traceback (most recent call last): ... - TypeError: h() argument after * must be an iterable, not function + TypeError: test.test_extcall.h() argument after * must be an iterable, not function >>> dir(*h) Traceback (most recent call last): diff --git a/Objects/descrobject.c b/Objects/descrobject.c index f2a0484f85fec1..53cae3da65332e 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -239,7 +239,7 @@ method_check_args(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObj PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { PyErr_Format(PyExc_TypeError, - "%U needs an argument", funcstr); + "unbound method %U needs an argument", funcstr); Py_DECREF(funcstr); } return -1; diff --git a/Objects/object.c b/Objects/object.c index 512384f39c6000..6a518f958c39dc 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -665,28 +665,56 @@ PyObject_Bytes(PyObject *v) /* -def _PyObject_FunctionStr(f): +def _PyObject_FunctionStr(x): try: - return f.__qualname__ + "()" - except Exception: - return str(f) + qualname = x.__qualname__ + except AttributeError: + return str(x) + try: + mod = x.__module__ + if mod is not None and mod != 'builtins': + return f"{x.__module__}.{qualname}()" + except AttributeError: + pass + return qualname */ PyObject * -_PyObject_FunctionStr(PyObject *f) +_PyObject_FunctionStr(PyObject *x) { + _Py_IDENTIFIER(__module__); _Py_IDENTIFIER(__qualname__); - PyObject *name = _PyObject_GetAttrId(f, &PyId___qualname__); - if (name != NULL) { - PyObject *res = PyUnicode_FromFormat("%S()", name); - Py_DECREF(name); - return res; + _Py_IDENTIFIER(builtins); + assert(!PyErr_Occurred()); + PyObject *qualname; + int ret = _PyObject_LookupAttrId(x, &PyId___qualname__, &qualname); + if (qualname == NULL) { + if (ret < 0) { + return NULL; + } + return PyObject_Str(x); + } + PyObject *module; + PyObject *result = NULL; + ret = _PyObject_LookupAttrId(x, &PyId___module__, &module); + if (module != NULL && module != Py_None) { + PyObject *builtinsname = _PyUnicode_FromId(&PyId_builtins); + if (builtinsname == NULL) { + goto done; + } + ret = PyObject_RichCompareBool(module, builtinsname, Py_NE); + if (ret > 0) { + result = PyUnicode_FromFormat("%S.%S()", module, qualname); + goto done; + } } - /* __qualname__ lookup failed */ - if (!PyErr_ExceptionMatches(PyExc_Exception)) { - return NULL; + else if (ret < 0) { + goto done; } - PyErr_Clear(); - return PyObject_Str(f); + result = PyUnicode_FromFormat("%S()", qualname); +done: + Py_DECREF(qualname); + Py_XDECREF(module); + return result; } /* For Python 3.0.1 and later, the old three-way comparison has been diff --git a/Python/ceval.c b/Python/ceval.c index cc93155d323e87..0b214a4072ff1f 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -5341,8 +5341,9 @@ static int check_args_iterable(PyThreadState *tstate, PyObject *func, PyObject *args) { if (args->ob_type->tp_iter == NULL && !PySequence_Check(args)) { - /* check_args_iterable() may be called with a live exception, - * clear it. */ + /* check_args_iterable() may be called with a live exception: + * clear it to prevent calling _PyObject_FunctionStr() with an + * exception set. */ PyErr_Clear(); PyObject *funcstr = _PyObject_FunctionStr(func); if (funcstr != NULL) { From b2e56da2083a18d90114097d63f448a1c1235f8b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 22 Oct 2019 14:14:39 +0200 Subject: [PATCH 6/8] Handle error in PyObject_RichCompareBool --- Objects/object.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Objects/object.c b/Objects/object.c index 6a518f958c39dc..593962a2b63495 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -702,6 +702,10 @@ _PyObject_FunctionStr(PyObject *x) goto done; } ret = PyObject_RichCompareBool(module, builtinsname, Py_NE); + if (ret < 0) { + // error + goto done; + } if (ret > 0) { result = PyUnicode_FromFormat("%S.%S()", module, qualname); goto done; From 21aee8001f4e3cf9b7edbb5e8e34cc965dec9f44 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 22 Oct 2019 14:15:11 +0200 Subject: [PATCH 7/8] Add author to NEWS --- Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst index 5e20d05ff30c36..2a6efaaaeafae5 100644 --- a/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst +++ b/Misc/NEWS.d/next/C API/2019-07-21-21-08-47.bpo-37645.4DcUaI.rst @@ -1,2 +1,2 @@ Add :c:func:`_PyObject_FunctionStr` to get a user-friendly string representation -of a function-like object. +of a function-like object. Patch by Jeroen Demeyer. From 402ec96210412af6442544c3f954e3d0532519c8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 5 Nov 2019 15:51:29 +0100 Subject: [PATCH 8/8] Change format_kwargs_error to use _PyObject_FunctionStr as well --- Lib/test/test_extcall.py | 26 +++++++++++++------------- Lib/test/test_unpack_ex.py | 10 +++++----- Python/ceval.c | 32 +++++++++++++++++++------------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index c1fcf623d713ec..4edb6680e0f989 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -52,15 +52,15 @@ >>> f(1, 2, **{'a': -1, 'b': 5}, **{'a': 4, 'c': 6}) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'a' + TypeError: test.test_extcall.f() got multiple values for keyword argument 'a' >>> f(1, 2, **{'a': -1, 'b': 5}, a=4, c=6) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'a' + TypeError: test.test_extcall.f() got multiple values for keyword argument 'a' >>> f(1, 2, a=3, **{'a': 4}, **{'a': 5}) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'a' + TypeError: test.test_extcall.f() got multiple values for keyword argument 'a' >>> f(1, 2, 3, *[4, 5], **{'a':6, 'b':7}) (1, 2, 3, 4, 5) {'a': 6, 'b': 7} >>> f(1, 2, 3, x=4, y=5, *(6, 7), **{'a':8, 'b': 9}) @@ -274,32 +274,32 @@ >>> h(**h) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not function + TypeError: test.test_extcall.h() argument after ** must be a mapping, not function >>> h(**[]) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not list + TypeError: test.test_extcall.h() argument after ** must be a mapping, not list >>> h(a=1, **h) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not function + TypeError: test.test_extcall.h() argument after ** must be a mapping, not function >>> h(a=1, **[]) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not list + TypeError: test.test_extcall.h() argument after ** must be a mapping, not list >>> h(**{'a': 1}, **h) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not function + TypeError: test.test_extcall.h() argument after ** must be a mapping, not function >>> h(**{'a': 1}, **[]) Traceback (most recent call last): ... - TypeError: h() argument after ** must be a mapping, not list + TypeError: test.test_extcall.h() argument after ** must be a mapping, not list >>> dir(**h) Traceback (most recent call last): @@ -309,7 +309,7 @@ >>> nothing(**h) Traceback (most recent call last): ... - TypeError: NoneType object argument after ** must be a mapping, \ + TypeError: None argument after ** must be a mapping, \ not function >>> dir(b=1, **{'b': 1}) @@ -351,17 +351,17 @@ >>> g(**MultiDict([('x', 1), ('x', 2)])) Traceback (most recent call last): ... - TypeError: g() got multiple values for keyword argument 'x' + TypeError: test.test_extcall.g() got multiple values for keyword argument 'x' >>> g(a=3, **MultiDict([('x', 1), ('x', 2)])) Traceback (most recent call last): ... - TypeError: g() got multiple values for keyword argument 'x' + TypeError: test.test_extcall.g() got multiple values for keyword argument 'x' >>> g(**MultiDict([('a', 3)]), **MultiDict([('x', 1), ('x', 2)])) Traceback (most recent call last): ... - TypeError: g() got multiple values for keyword argument 'x' + TypeError: test.test_extcall.g() got multiple values for keyword argument 'x' Another helper function diff --git a/Lib/test/test_unpack_ex.py b/Lib/test/test_unpack_ex.py index 87fea593c0201b..46f70c2b98c709 100644 --- a/Lib/test/test_unpack_ex.py +++ b/Lib/test/test_unpack_ex.py @@ -236,27 +236,27 @@ >>> f(x=5, **{'x': 3}, y=2) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'x' + TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x' >>> f(**{'x': 3}, x=5, y=2) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'x' + TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x' >>> f(**{'x': 3}, **{'x': 5}, y=2) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'x' + TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x' >>> f(x=5, **{'x': 3}, **{'x': 2}) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument 'x' + TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x' >>> f(**{1: 3}, **{1: 5}) Traceback (most recent call last): ... - TypeError: f() got multiple values for keyword argument '1' + TypeError: test.test_unpack_ex.f() got multiple values for keyword argument '1' Unpacking non-sequence diff --git a/Python/ceval.c b/Python/ceval.c index 0b214a4072ff1f..0b18a579cded6c 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -5367,24 +5367,30 @@ format_kwargs_error(PyThreadState *tstate, PyObject *func, PyObject *kwargs) * is not a mapping. */ if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) { - _PyErr_Format(tstate, PyExc_TypeError, - "%.200s%.200s argument after ** " - "must be a mapping, not %.200s", - PyEval_GetFuncName(func), - PyEval_GetFuncDesc(func), - kwargs->ob_type->tp_name); + PyErr_Clear(); + PyObject *funcstr = _PyObject_FunctionStr(func); + if (funcstr != NULL) { + _PyErr_Format( + tstate, PyExc_TypeError, + "%U argument after ** must be a mapping, not %.200s", + funcstr, Py_TYPE(kwargs)->tp_name); + Py_DECREF(funcstr); + } } else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { PyObject *exc, *val, *tb; _PyErr_Fetch(tstate, &exc, &val, &tb); if (val && PyTuple_Check(val) && PyTuple_GET_SIZE(val) == 1) { - PyObject *key = PyTuple_GET_ITEM(val, 0); - _PyErr_Format(tstate, PyExc_TypeError, - "%.200s%.200s got multiple " - "values for keyword argument '%S'", - PyEval_GetFuncName(func), - PyEval_GetFuncDesc(func), - key); + PyErr_Clear(); + PyObject *funcstr = _PyObject_FunctionStr(func); + if (funcstr != NULL) { + PyObject *key = PyTuple_GET_ITEM(val, 0); + _PyErr_Format( + tstate, PyExc_TypeError, + "%U got multiple values for keyword argument '%S'", + funcstr, key); + Py_DECREF(funcstr); + } Py_XDECREF(exc); Py_XDECREF(val); Py_XDECREF(tb);