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
39 changes: 37 additions & 2 deletions 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 @@ -51,7 +52,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_GetAttrString` instead.
For proper error handling, use :c:func:`PyObject_GetOptionalAttrString`
or :c:func:`PyObject_GetAttrString` instead.


.. c:function:: PyObject* PyObject_GetAttr(PyObject *o, PyObject *attr_name)
Expand All @@ -60,13 +62,46 @@ 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:: PyObject* PyObject_GetAttrString(PyObject *o, const char *attr_name)

Retrieve an attribute named *attr_name* from object *o*. Returns the attribute
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_GetOptionalAttrString` instead.


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

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

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

.. versionadded:: 3.13


.. c:function:: int PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result);

Copy link
Member

Choose a reason for hiding this comment

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

You may just say:

Similar to :c:func:`PyObject_GetOptionalAttr`, but *name* is a UTF-8 encoded string.

To not copy/paste the documentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

In all other functions with String suffix the documentation is repeated. If we are going to change this, it should be changed for all function at once.

Variant of :c:func:`PyObject_GetAttrString` which doesn't raise
:exc:`AttributeError`.

Return ``1`` and set ``*result != NULL`` if the attribute is found.
Return ``0`` and set ``*result == NULL`` if the attribute is not found;
the :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_GenericGetAttr(PyObject *o, PyObject *name)

Expand Down
2 changes: 2 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.

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

* Add :c:func:`PyObject_GetOptionalAttr` and
:c:func:`PyObject_GetOptionalAttrString`, variants of
:c:func:`PyObject_GetAttr` and :c:func:`PyObject_GetAttrString` which
don't raise :exc:`AttributeError`.
These variants are more 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
28 changes: 28 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ 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);

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

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


/* Implemented elsewhere:

int PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result);

Variant of PyObject_GetAttrString() which doesn't raise AttributeError.

Return 1 and set *result != NULL if the attribute is found.
Return 0 and set *result == NULL if the attribute is not found;
the 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
4 changes: 4 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ 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 **);
PyAPI_FUNC(int) PyObject_GetOptionalAttrString(PyObject *, const char *, 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
2 changes: 2 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 @@
Add :c:func:`PyObject_GetOptionalAttr` and :c:func:`PyObject_GetOptionalAttrString` functions.
4 changes: 4 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2432,3 +2432,7 @@
added = '3.13'
[function.PyWeakref_GetRef]
added = '3.13'
[function.PyObject_GetOptionalAttr]
added = '3.13'
[function.PyObject_GetOptionalAttrString]
added = '3.13'
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 @@ -248,7 +248,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 @@ -2966,7 +2966,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
Loading