Skip to content

Commit

Permalink
gh-108511: Add C API functions which do not silently ignore errors (G…
Browse files Browse the repository at this point in the history
…H-109025)

Add the following functions:

* PyObject_HasAttrWithError()
* PyObject_HasAttrStringWithError()
* PyMapping_HasKeyWithError()
* PyMapping_HasKeyStringWithError()
  • Loading branch information
serhiy-storchaka committed Sep 17, 2023
1 parent e57ecf6 commit add16f1
Show file tree
Hide file tree
Showing 28 changed files with 330 additions and 111 deletions.
25 changes: 22 additions & 3 deletions Doc/c-api/mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
rather than a :c:expr:`PyObject*`.
.. c:function:: int PyMapping_HasKeyWithError(PyObject *o, PyObject *key)
Return ``1`` if the mapping object has the key *key* and ``0`` otherwise.
This is equivalent to the Python expression ``key in o``.
On failure, return ``-1``.
.. versionadded:: 3.13
.. c:function:: int PyMapping_HasKeyStringWithError(PyObject *o, const char *key)
This is the same as :c:func:`PyMapping_HasKeyWithError`, but *key* is
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
rather than a :c:expr:`PyObject*`.
.. versionadded:: 3.13
.. c:function:: int PyMapping_HasKey(PyObject *o, PyObject *key)
Return ``1`` if the mapping object has the key *key* and ``0`` otherwise.
Expand All @@ -86,8 +104,8 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
Exceptions which occur when this calls :meth:`~object.__getitem__`
method are silently ignored.
For proper error handling, use :c:func:`PyMapping_GetOptionalItem` or
:c:func:`PyObject_GetItem()` instead.
For proper error handling, use :c:func:`PyMapping_HasKeyWithError`,
:c:func:`PyMapping_GetOptionalItem` or :c:func:`PyObject_GetItem()` instead.
.. c:function:: int PyMapping_HasKeyString(PyObject *o, const char *key)
Expand All @@ -101,7 +119,8 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
Exceptions that occur when this calls :meth:`~object.__getitem__`
method or while creating the temporary :class:`str`
object are silently ignored.
For proper error handling, use :c:func:`PyMapping_GetOptionalItemString` or
For proper error handling, use :c:func:`PyMapping_HasKeyStringWithError`,
:c:func:`PyMapping_GetOptionalItemString` or
:c:func:`PyMapping_GetItemString` instead.
Expand Down
25 changes: 22 additions & 3 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ Object Protocol
instead of the :func:`repr`.
.. c:function:: int PyObject_HasAttrWithError(PyObject *o, const char *attr_name)
Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.
This is equivalent to the Python expression ``hasattr(o, attr_name)``.
On failure, return ``-1``.
.. versionadded:: 3.13
.. c:function:: int PyObject_HasAttrStringWithError(PyObject *o, const char *attr_name)
This is the same as :c:func:`PyObject_HasAttrWithError`, but *attr_name* is
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
rather than a :c:expr:`PyObject*`.
.. versionadded:: 3.13
.. c:function:: int PyObject_HasAttr(PyObject *o, PyObject *attr_name)
Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise. This
Expand All @@ -37,8 +55,8 @@ Object Protocol
Exceptions that occur when this calls :meth:`~object.__getattr__` and
:meth:`~object.__getattribute__` methods are silently ignored.
For proper error handling, use :c:func:`PyObject_GetOptionalAttr` or
:c:func:`PyObject_GetAttr` instead.
For proper error handling, use :c:func:`PyObject_HasAttrWithError`,
:c:func:`PyObject_GetOptionalAttr` or :c:func:`PyObject_GetAttr` instead.
.. c:function:: int PyObject_HasAttrString(PyObject *o, const char *attr_name)
Expand All @@ -52,7 +70,8 @@ Object Protocol
Exceptions that occur when this calls :meth:`~object.__getattr__` and
:meth:`~object.__getattribute__` methods or while creating the temporary
:class:`str` object are silently ignored.
For proper error handling, use :c:func:`PyObject_GetOptionalAttrString`
For proper error handling, use :c:func:`PyObject_HasAttrStringWithError`,
:c:func:`PyObject_GetOptionalAttrString`
or :c:func:`PyObject_GetAttrString` instead.
Expand Down
4 changes: 4 additions & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,18 @@ New Features
be treated as a failure.
(Contributed by Serhiy Storchaka in :gh:`106307`.)

* Add fixed variants of functions which silently ignore errors:

- :c:func:`PyObject_HasAttrWithError` replaces :c:func:`PyObject_HasAttr`.
- :c:func:`PyObject_HasAttrStringWithError` replaces :c:func:`PyObject_HasAttrString`.
- :c:func:`PyMapping_HasKeyWithError` replaces :c:func:`PyMapping_HasKey`.
- :c:func:`PyMapping_HasKeyStringWithError` replaces :c:func:`PyMapping_HasKeyString`.

New functions return not only ``1`` for true and ``0`` for false, but also
``-1`` for error.

(Contributed by Serhiy Storchaka in :gh:`108511`.)

* If Python is built in :ref:`debug mode <debug-build>` or :option:`with
assertions <--with-assertions>`, :c:func:`PyTuple_SET_ITEM` and
:c:func:`PyList_SET_ITEM` now check the index argument with an assertion.
Expand Down
31 changes: 31 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ extern "C" {
This function always succeeds. */


/* Implemented elsewhere:
int PyObject_HasAttrStringWithError(PyObject *o, const char *attr_name);
Returns 1 if object 'o' has the attribute attr_name, and 0 otherwise.
This is equivalent to the Python expression: hasattr(o,attr_name).
Returns -1 on failure. */


/* Implemented elsewhere:
int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name);
Returns 1 if o has the attribute attr_name, and 0 otherwise.
This is equivalent to the Python expression: hasattr(o,attr_name).
Returns -1 on failure. */


/* Implemented elsewhere:
PyObject* PyObject_GetAttr(PyObject *o, PyObject *attr_name);
Expand Down Expand Up @@ -821,6 +840,18 @@ PyAPI_FUNC(int) PyMapping_HasKeyString(PyObject *o, const char *key);
This function always succeeds. */
PyAPI_FUNC(int) PyMapping_HasKey(PyObject *o, PyObject *key);

/* Return 1 if the mapping object has the key 'key', and 0 otherwise.
This is equivalent to the Python expression: key in o.
On failure, return -1. */

PyAPI_FUNC(int) PyMapping_HasKeyWithError(PyObject *o, PyObject *key);

/* Return 1 if the mapping object has the key 'key', and 0 otherwise.
This is equivalent to the Python expression: key in o.
On failure, return -1. */

PyAPI_FUNC(int) PyMapping_HasKeyStringWithError(PyObject *o, const char *key);

/* On success, return a list or tuple of the keys in mapping object 'o'.
On failure, return NULL. */
PyAPI_FUNC(PyObject *) PyMapping_Keys(PyObject *o);
Expand Down
4 changes: 4 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,10 @@ PyAPI_FUNC(int) PyObject_GetOptionalAttrString(PyObject *, const char *, PyObjec
PyAPI_FUNC(int) PyObject_SetAttr(PyObject *, PyObject *, PyObject *);
PyAPI_FUNC(int) PyObject_DelAttr(PyObject *v, PyObject *name);
PyAPI_FUNC(int) PyObject_HasAttr(PyObject *, PyObject *);
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
PyAPI_FUNC(int) PyObject_HasAttrWithError(PyObject *, PyObject *);
PyAPI_FUNC(int) PyObject_HasAttrStringWithError(PyObject *, const char *);
#endif
PyAPI_FUNC(PyObject *) PyObject_SelfIter(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_GenericGetAttr(PyObject *, PyObject *);
PyAPI_FUNC(int) PyObject_GenericSetAttr(PyObject *, PyObject *, PyObject *);
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_capi/test_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,34 @@ def test_object_hasattrstring(self):
# CRASHES hasattrstring(obj, NULL)
# CRASHES hasattrstring(NULL, b'a')

def test_object_hasattrwitherror(self):
xhasattr = _testcapi.object_hasattrwitherror
obj = TestObject()
obj.a = 1
setattr(obj, '\U0001f40d', 2)
self.assertTrue(xhasattr(obj, 'a'))
self.assertFalse(xhasattr(obj, 'b'))
self.assertTrue(xhasattr(obj, '\U0001f40d'))

self.assertRaises(RuntimeError, xhasattr, obj, 'evil')
self.assertRaises(TypeError, xhasattr, obj, 1)
# CRASHES xhasattr(obj, NULL)
# CRASHES xhasattr(NULL, 'a')

def test_object_hasattrstringwitherror(self):
hasattrstring = _testcapi.object_hasattrstringwitherror
obj = TestObject()
obj.a = 1
setattr(obj, '\U0001f40d', 2)
self.assertTrue(hasattrstring(obj, b'a'))
self.assertFalse(hasattrstring(obj, b'b'))
self.assertTrue(hasattrstring(obj, '\U0001f40d'.encode()))

self.assertRaises(RuntimeError, hasattrstring, obj, b'evil')
self.assertRaises(UnicodeDecodeError, hasattrstring, obj, b'\xff')
# CRASHES hasattrstring(obj, NULL)
# CRASHES hasattrstring(NULL, b'a')

def test_object_setattr(self):
xsetattr = _testcapi.object_setattr
obj = TestObject()
Expand Down Expand Up @@ -339,6 +367,44 @@ def test_mapping_haskeystring(self):
self.assertFalse(haskeystring([], b'a'))
self.assertFalse(haskeystring(NULL, b'a'))

def test_mapping_haskeywitherror(self):
haskey = _testcapi.mapping_haskeywitherror
dct = {'a': 1, '\U0001f40d': 2}
self.assertTrue(haskey(dct, 'a'))
self.assertFalse(haskey(dct, 'b'))
self.assertTrue(haskey(dct, '\U0001f40d'))

dct2 = ProxyGetItem(dct)
self.assertTrue(haskey(dct2, 'a'))
self.assertFalse(haskey(dct2, 'b'))

self.assertTrue(haskey(['a', 'b', 'c'], 1))

self.assertRaises(TypeError, haskey, 42, 'a')
self.assertRaises(TypeError, haskey, {}, []) # unhashable
self.assertRaises(IndexError, haskey, [], 1)
self.assertRaises(TypeError, haskey, [], 'a')

# CRASHES haskey({}, NULL))
# CRASHES haskey(NULL, 'a'))

def test_mapping_haskeystringwitherror(self):
haskeystring = _testcapi.mapping_haskeystringwitherror
dct = {'a': 1, '\U0001f40d': 2}
self.assertTrue(haskeystring(dct, b'a'))
self.assertFalse(haskeystring(dct, b'b'))
self.assertTrue(haskeystring(dct, '\U0001f40d'.encode()))

dct2 = ProxyGetItem(dct)
self.assertTrue(haskeystring(dct2, b'a'))
self.assertFalse(haskeystring(dct2, b'b'))

self.assertRaises(TypeError, haskeystring, 42, b'a')
self.assertRaises(UnicodeDecodeError, haskeystring, {}, b'\xff')
self.assertRaises(SystemError, haskeystring, {}, NULL)
self.assertRaises(TypeError, haskeystring, [], b'a')
# CRASHES haskeystring(NULL, b'a')

def test_object_setitem(self):
setitem = _testcapi.object_setitem
dct = {}
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add functions :c:func:`PyObject_HasAttrWithError`,
:c:func:`PyObject_HasAttrStringWithError`,
:c:func:`PyMapping_HasKeyWithError` and
:c:func:`PyMapping_HasKeyStringWithError`.
8 changes: 8 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2452,3 +2452,11 @@
added = '3.13'
[function.PyLong_AsInt]
added = '3.13'
[function.PyObject_HasAttrWithError]
added = '3.13'
[function.PyObject_HasAttrStringWithError]
added = '3.13'
[function.PyMapping_HasKeyWithError]
added = '3.13'
[function.PyMapping_HasKeyStringWithError]
added = '3.13'
6 changes: 3 additions & 3 deletions Modules/_ctypes/stgdict.c
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,11 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
if (fields == NULL)
return 0;

if (PyObject_GetOptionalAttr(type, &_Py_ID(_swappedbytes_), &tmp) < 0) {
int rc = PyObject_HasAttrWithError(type, &_Py_ID(_swappedbytes_));
if (rc < 0) {
return -1;
}
if (tmp) {
Py_DECREF(tmp);
if (rc) {
big_endian = !PY_BIG_ENDIAN;
}
else {
Expand Down
3 changes: 1 addition & 2 deletions Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -3532,12 +3532,11 @@ expat_start_doctype_handler(XMLParserObject *self,
sysid_obj, NULL);
Py_XDECREF(res);
}
else if (PyObject_GetOptionalAttr((PyObject *)self, st->str_doctype, &res) > 0) {
else if (PyObject_HasAttrWithError((PyObject *)self, st->str_doctype) > 0) {
(void)PyErr_WarnEx(PyExc_RuntimeWarning,
"The doctype() method of XMLParser is ignored. "
"Define doctype() method on the TreeBuilder target.",
1);
Py_DECREF(res);
}

Py_DECREF(doctype_name_obj);
Expand Down
6 changes: 1 addition & 5 deletions Modules/_io/iobase.c
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,9 @@ _io__IOBase_truncate_impl(PyObject *self, PyTypeObject *cls,
static int
iobase_is_closed(PyObject *self)
{
PyObject *res;
int ret;
/* This gets the derived attribute, which is *not* __IOBase_closed
in most cases! */
ret = PyObject_GetOptionalAttr(self, &_Py_ID(__IOBase_closed), &res);
Py_XDECREF(res);
return ret;
return PyObject_HasAttrWithError(self, &_Py_ID(__IOBase_closed));
}

/* Flush and close methods */
Expand Down
3 changes: 1 addition & 2 deletions Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1223,11 +1223,10 @@ _io_TextIOWrapper___init___impl(textio *self, PyObject *buffer,
goto error;
self->seekable = self->telling = r;

r = PyObject_GetOptionalAttr(buffer, &_Py_ID(read1), &res);
r = PyObject_HasAttrWithError(buffer, &_Py_ID(read1));
if (r < 0) {
goto error;
}
Py_XDECREF(res);
self->has_read1 = r;

self->encoding_start_of_stream = 0;
Expand Down
7 changes: 3 additions & 4 deletions Modules/_pickle.c
Original file line number Diff line number Diff line change
Expand Up @@ -5799,14 +5799,13 @@ instantiate(PyObject *cls, PyObject *args)
into a newly created tuple. */
assert(PyTuple_Check(args));
if (!PyTuple_GET_SIZE(args) && PyType_Check(cls)) {
PyObject *func;
if (PyObject_GetOptionalAttr(cls, &_Py_ID(__getinitargs__), &func) < 0) {
int rc = PyObject_HasAttrWithError(cls, &_Py_ID(__getinitargs__));
if (rc < 0) {
return NULL;
}
if (func == NULL) {
if (!rc) {
return PyObject_CallMethodOneArg(cls, &_Py_ID(__new__), cls);
}
Py_DECREF(func);
}
return PyObject_CallObject(cls, args);
}
Expand Down

0 comments on commit add16f1

Please sign in to comment.