Skip to content

gh-106521: Add PyObject_GetOptionalAttr() function #106522

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
19 changes: 18 additions & 1 deletion Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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_GetAttr` instead.
For proper error handling, use :c:func:`PyObject_GetOptionalAttr` or
:c:func:`PyObject_GetAttr` instead.


.. c:function:: int PyObject_HasAttrString(PyObject *o, const char *attr_name)
Expand All @@ -60,6 +61,22 @@ Object Protocol
value on success, or ``NULL`` on failure. This is the equivalent of the Python
expression ``o.attr_name``.

If the missing attribute should not be treated as a failure, you can use
:c:func:`PyObject_GetOptionalAttr` instead.


.. c:function:: int PyObject_GetOptionalAttr(PyObject *obj, PyObject *attr_name, PyObject **result);

Replacement of :c:func:`PyObject_GetAttr` which doesn't raise
:exc:`AttributeError`.

Return ``1`` and set ``*result != NULL`` if an attribute is found.
Return ``0`` and set ``*result == NULL`` if an attribute is not found;
an :exc:`AttributeError` is silenced.
Return ``-1`` and set ``*result == NULL`` if an error other than
:exc:`AttributeError` is raised.

.. versionadded:: 3.13

.. c:function:: PyObject* PyObject_GetAttrString(PyObject *o, const char *attr_name)

Expand Down
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

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

6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ New Features
``NULL`` if the referent is no longer live.
(Contributed by Victor Stinner in :gh:`105927`.)

* Add :c:func:`PyObject_GetOptionalAttr` function: replacement of
:c:func:`PyObject_GetAttr` which doesn't raise :exc:`AttributeError`.
It is convenient and faster if the missing attribute should not be treated
as a failure.
(Contributed by Serhiy Storchaka in :gh:`106521`.)

* 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
14 changes: 14 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ extern "C" {
This is the equivalent of the Python expression: o.attr_name. */


/* Implemented elsewhere:

int PyObject_GetOptionalAttr(PyObject *obj, PyObject *attr_name, PyObject **result);

Replacement of PyObject_GetAttr() which doesn't raise AttributeError.

Return 1 and set *result != NULL if an attribute is found.
Return 0 and set *result == NULL if an attribute is not found;
an AttributeError is silenced.
Return -1 and set *result == NULL if an error other than AttributeError
is raised.
*/


/* Implemented elsewhere:

int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v);
Expand Down
11 changes: 0 additions & 11 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -293,17 +293,6 @@ PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
PyAPI_FUNC(int) _PyObject_IsAbstract(PyObject *);
PyAPI_FUNC(PyObject *) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);
PyAPI_FUNC(int) _PyObject_SetAttrId(PyObject *, _Py_Identifier *, PyObject *);
/* Replacements of PyObject_GetAttr() and _PyObject_GetAttrId() which
don't raise AttributeError.

Return 1 and set *result != NULL if an attribute is found.
Return 0 and set *result == NULL if an attribute is not found;
an AttributeError is silenced.
Return -1 and set *result == NULL if an error other than AttributeError
is raised.
*/
PyAPI_FUNC(int) _PyObject_LookupAttr(PyObject *, PyObject *, PyObject **);
PyAPI_FUNC(int) _PyObject_LookupAttrId(PyObject *, _Py_Identifier *, PyObject **);

PyAPI_FUNC(int) _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method);

Expand Down
3 changes: 3 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ PyAPI_FUNC(PyObject *) PyObject_GetAttrString(PyObject *, const char *);
PyAPI_FUNC(int) PyObject_SetAttrString(PyObject *, const char *, PyObject *);
PyAPI_FUNC(int) PyObject_HasAttrString(PyObject *, const char *);
PyAPI_FUNC(PyObject *) PyObject_GetAttr(PyObject *, PyObject *);
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
PyAPI_FUNC(int) PyObject_GetOptionalAttr(PyObject *, PyObject *, PyObject **);
#endif
PyAPI_FUNC(int) PyObject_SetAttr(PyObject *, PyObject *, PyObject *);
PyAPI_FUNC(int) PyObject_HasAttr(PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) PyObject_SelfIter(PyObject *);
Expand Down
1 change: 1 addition & 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 @@
Add :c:func:`PyObject_GetOptionalAttr` function.
2 changes: 2 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,8 @@
added = '3.2'
[function.PyObject_GetIter]
added = '3.2'
[function.PyObject_GetOptionalAttr]
added = '3.13'
[function.PyObject_HasAttr]
added = '3.2'
[function.PyObject_HasAttrString]
Expand Down
4 changes: 2 additions & 2 deletions Modules/_abc.c
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ compute_abstract_methods(PyObject *self)
PyObject *item = PyTuple_GET_ITEM(bases, pos); // borrowed
PyObject *base_abstracts, *iter;

if (_PyObject_LookupAttr(item, &_Py_ID(__abstractmethods__),
if (PyObject_GetOptionalAttr(item, &_Py_ID(__abstractmethods__),
&base_abstracts) < 0) {
goto error;
}
Expand All @@ -375,7 +375,7 @@ compute_abstract_methods(PyObject *self)
Py_DECREF(base_abstracts);
PyObject *key, *value;
while ((key = PyIter_Next(iter))) {
if (_PyObject_LookupAttr(self, key, &value) < 0) {
if (PyObject_GetOptionalAttr(self, key, &value) < 0) {
Py_DECREF(key);
Py_DECREF(iter);
goto error;
Expand Down
4 changes: 2 additions & 2 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ get_future_loop(asyncio_state *state, PyObject *fut)
return Py_NewRef(loop);
}

if (_PyObject_LookupAttr(fut, &_Py_ID(get_loop), &getloop) < 0) {
if (PyObject_GetOptionalAttr(fut, &_Py_ID(get_loop), &getloop) < 0) {
return NULL;
}
if (getloop != NULL) {
Expand Down Expand Up @@ -2981,7 +2981,7 @@ task_step_handle_result_impl(asyncio_state *state, TaskObj *task, PyObject *resu
}

/* Check if `result` is a Future-compatible object */
if (_PyObject_LookupAttr(result, &_Py_ID(_asyncio_future_blocking), &o) < 0) {
if (PyObject_GetOptionalAttr(result, &_Py_ID(_asyncio_future_blocking), &o) < 0) {
goto fail;
}
if (o != NULL && o != Py_None) {
Expand Down
2 changes: 1 addition & 1 deletion Modules/_csv.c
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,7 @@ csv_writer(PyObject *module, PyObject *args, PyObject *keyword_args)
Py_DECREF(self);
return NULL;
}
if (_PyObject_LookupAttr(output_file,
if (PyObject_GetOptionalAttr(output_file,
module_state->str_write,
&self->write) < 0) {
Py_DECREF(self);
Expand Down
22 changes: 11 additions & 11 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ CDataType_from_param(PyObject *type, PyObject *value)
return NULL;
}

if (_PyObject_LookupAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
return NULL;
}
if (as_parameter) {
Expand Down Expand Up @@ -1495,7 +1495,7 @@ PyCArrayType_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
stgdict = NULL;
type_attr = NULL;

if (_PyObject_LookupAttr((PyObject *)result, &_Py_ID(_length_), &length_attr) < 0) {
if (PyObject_GetOptionalAttr((PyObject *)result, &_Py_ID(_length_), &length_attr) < 0) {
goto error;
}
if (!length_attr) {
Expand Down Expand Up @@ -1528,7 +1528,7 @@ PyCArrayType_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
goto error;
}

if (_PyObject_LookupAttr((PyObject *)result, &_Py_ID(_type_), &type_attr) < 0) {
if (PyObject_GetOptionalAttr((PyObject *)result, &_Py_ID(_type_), &type_attr) < 0) {
goto error;
}
if (!type_attr) {
Expand Down Expand Up @@ -1720,7 +1720,7 @@ c_wchar_p_from_param(PyObject *type, PyObject *value)
}
}

if (_PyObject_LookupAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
return NULL;
}
if (as_parameter) {
Expand Down Expand Up @@ -1784,7 +1784,7 @@ c_char_p_from_param(PyObject *type, PyObject *value)
}
}

if (_PyObject_LookupAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
return NULL;
}
if (as_parameter) {
Expand Down Expand Up @@ -1919,7 +1919,7 @@ c_void_p_from_param(PyObject *type, PyObject *value)
}
}

if (_PyObject_LookupAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
return NULL;
}
if (as_parameter) {
Expand Down Expand Up @@ -2054,7 +2054,7 @@ PyCSimpleType_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
if (result == NULL)
return NULL;

if (_PyObject_LookupAttr((PyObject *)result, &_Py_ID(_type_), &proto) < 0) {
if (PyObject_GetOptionalAttr((PyObject *)result, &_Py_ID(_type_), &proto) < 0) {
return NULL;
}
if (!proto) {
Expand Down Expand Up @@ -2266,7 +2266,7 @@ PyCSimpleType_from_param(PyObject *type, PyObject *value)
PyObject *exc = PyErr_GetRaisedException();
Py_DECREF(parg);

if (_PyObject_LookupAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
Py_XDECREF(exc);
return NULL;
}
Expand Down Expand Up @@ -2429,7 +2429,7 @@ converters_from_argtypes(PyObject *ob)
}
*/

if (_PyObject_LookupAttr(tp, &_Py_ID(from_param), &cnv) <= 0) {
if (PyObject_GetOptionalAttr(tp, &_Py_ID(from_param), &cnv) <= 0) {
Py_DECREF(converters);
Py_DECREF(ob);
if (!PyErr_Occurred()) {
Expand Down Expand Up @@ -2489,7 +2489,7 @@ make_funcptrtype_dict(StgDictObject *stgdict)
return -1;
}
stgdict->restype = Py_NewRef(ob);
if (_PyObject_LookupAttr(ob, &_Py_ID(_check_retval_),
if (PyObject_GetOptionalAttr(ob, &_Py_ID(_check_retval_),
&stgdict->checker) < 0)
{
return -1;
Expand Down Expand Up @@ -3275,7 +3275,7 @@ PyCFuncPtr_set_restype(PyCFuncPtrObject *self, PyObject *ob, void *Py_UNUSED(ign
"restype must be a type, a callable, or None");
return -1;
}
if (_PyObject_LookupAttr(ob, &_Py_ID(_check_retval_), &checker) < 0) {
if (PyObject_GetOptionalAttr(ob, &_Py_ID(_check_retval_), &checker) < 0) {
return -1;
}
oldchecker = self->checker;
Expand Down
2 changes: 1 addition & 1 deletion Modules/_ctypes/callproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ static int ConvParam(PyObject *obj, Py_ssize_t index, struct argument *pa)

{
PyObject *arg;
if (_PyObject_LookupAttr(obj, &_Py_ID(_as_parameter_), &arg) < 0) {
if (PyObject_GetOptionalAttr(obj, &_Py_ID(_as_parameter_), &arg) < 0) {
return -1;
}
/* Which types should we exactly allow here?
Expand Down
6 changes: 3 additions & 3 deletions Modules/_ctypes/stgdict.c
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ MakeAnonFields(PyObject *type)
PyObject *anon_names;
Py_ssize_t i;

if (_PyObject_LookupAttr(type, &_Py_ID(_anonymous_), &anon) < 0) {
if (PyObject_GetOptionalAttr(type, &_Py_ID(_anonymous_), &anon) < 0) {
return -1;
}
if (anon == NULL) {
Expand Down Expand Up @@ -385,7 +385,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
if (fields == NULL)
return 0;

if (_PyObject_LookupAttr(type, &_Py_ID(_swappedbytes_), &tmp) < 0) {
if (PyObject_GetOptionalAttr(type, &_Py_ID(_swappedbytes_), &tmp) < 0) {
return -1;
}
if (tmp) {
Expand All @@ -396,7 +396,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
big_endian = PY_BIG_ENDIAN;
}

if (_PyObject_LookupAttr(type, &_Py_ID(_pack_), &tmp) < 0) {
if (PyObject_GetOptionalAttr(type, &_Py_ID(_pack_), &tmp) < 0) {
return -1;
}
if (tmp) {
Expand Down
2 changes: 1 addition & 1 deletion Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3791,7 +3791,7 @@ tzinfo_reduce(PyObject *self, PyObject *Py_UNUSED(ignored))
PyObject *args, *state;
PyObject *getinitargs;

if (_PyObject_LookupAttr(self, &_Py_ID(__getinitargs__), &getinitargs) < 0) {
if (PyObject_GetOptionalAttr(self, &_Py_ID(__getinitargs__), &getinitargs) < 0) {
return NULL;
}
if (getinitargs != NULL) {
Expand Down
2 changes: 1 addition & 1 deletion Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -3530,7 +3530,7 @@ expat_start_doctype_handler(XMLParserObject *self,
sysid_obj, NULL);
Py_XDECREF(res);
}
else if (_PyObject_LookupAttr((PyObject *)self, st->str_doctype, &res) > 0) {
else if (PyObject_GetOptionalAttr((PyObject *)self, st->str_doctype, &res) > 0) {
(void)PyErr_WarnEx(PyExc_RuntimeWarning,
"The doctype() method of XMLParser is ignored. "
"Define doctype() method on the TreeBuilder target.",
Expand Down
4 changes: 2 additions & 2 deletions Modules/_io/bufferedio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1463,7 +1463,7 @@ buffered_repr(buffered *self)
{
PyObject *nameobj, *res;

if (_PyObject_LookupAttr((PyObject *) self, &_Py_ID(name), &nameobj) < 0) {
if (PyObject_GetOptionalAttr((PyObject *) self, &_Py_ID(name), &nameobj) < 0) {
if (!PyErr_ExceptionMatches(PyExc_ValueError)) {
return NULL;
}
Expand Down Expand Up @@ -1630,7 +1630,7 @@ _bufferedreader_read_all(buffered *self)
}
_bufferedreader_reset_buf(self);

if (_PyObject_LookupAttr(self->raw, &_Py_ID(readall), &readall) < 0) {
if (PyObject_GetOptionalAttr(self->raw, &_Py_ID(readall), &readall) < 0) {
goto cleanup;
}
if (readall) {
Expand Down
2 changes: 1 addition & 1 deletion Modules/_io/fileio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ fileio_repr(fileio *self)
if (self->fd < 0)
return PyUnicode_FromFormat("<_io.FileIO [closed]>");

if (_PyObject_LookupAttr((PyObject *) self, &_Py_ID(name), &nameobj) < 0) {
if (PyObject_GetOptionalAttr((PyObject *) self, &_Py_ID(name), &nameobj) < 0) {
return NULL;
}
if (nameobj == NULL) {
Expand Down
8 changes: 4 additions & 4 deletions Modules/_io/iobase.c
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ iobase_is_closed(PyObject *self)
int ret;
/* This gets the derived attribute, which is *not* __IOBase_closed
in most cases! */
ret = _PyObject_LookupAttr(self, &_Py_ID(__IOBase_closed), &res);
ret = PyObject_GetOptionalAttr(self, &_Py_ID(__IOBase_closed), &res);
Py_XDECREF(res);
return ret;
}
Expand Down Expand Up @@ -196,7 +196,7 @@ iobase_check_closed(PyObject *self)
int closed;
/* This gets the derived attribute, which is *not* __IOBase_closed
in most cases! */
closed = _PyObject_LookupAttr(self, &_Py_ID(closed), &res);
closed = PyObject_GetOptionalAttr(self, &_Py_ID(closed), &res);
if (closed > 0) {
closed = PyObject_IsTrue(res);
Py_DECREF(res);
Expand Down Expand Up @@ -303,7 +303,7 @@ iobase_finalize(PyObject *self)

/* If `closed` doesn't exist or can't be evaluated as bool, then the
object is probably in an unusable state, so ignore. */
if (_PyObject_LookupAttr(self, &_Py_ID(closed), &res) <= 0) {
if (PyObject_GetOptionalAttr(self, &_Py_ID(closed), &res) <= 0) {
PyErr_Clear();
closed = -1;
}
Expand Down Expand Up @@ -571,7 +571,7 @@ _io__IOBase_readline_impl(PyObject *self, Py_ssize_t limit)
PyObject *peek, *buffer, *result;
Py_ssize_t old_size = -1;

if (_PyObject_LookupAttr(self, &_Py_ID(peek), &peek) < 0) {
if (PyObject_GetOptionalAttr(self, &_Py_ID(peek), &peek) < 0) {
return NULL;
}

Expand Down
Loading