Skip to content

Commit add16f1

Browse files
gh-108511: Add C API functions which do not silently ignore errors (GH-109025)
Add the following functions: * PyObject_HasAttrWithError() * PyObject_HasAttrStringWithError() * PyMapping_HasKeyWithError() * PyMapping_HasKeyStringWithError()
1 parent e57ecf6 commit add16f1

28 files changed

+330
-111
lines changed

Doc/c-api/mapping.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
7676
rather than a :c:expr:`PyObject*`.
7777
7878
79+
.. c:function:: int PyMapping_HasKeyWithError(PyObject *o, PyObject *key)
80+
81+
Return ``1`` if the mapping object has the key *key* and ``0`` otherwise.
82+
This is equivalent to the Python expression ``key in o``.
83+
On failure, return ``-1``.
84+
85+
.. versionadded:: 3.13
86+
87+
88+
.. c:function:: int PyMapping_HasKeyStringWithError(PyObject *o, const char *key)
89+
90+
This is the same as :c:func:`PyMapping_HasKeyWithError`, but *key* is
91+
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
92+
rather than a :c:expr:`PyObject*`.
93+
94+
.. versionadded:: 3.13
95+
96+
7997
.. c:function:: int PyMapping_HasKey(PyObject *o, PyObject *key)
8098
8199
Return ``1`` if the mapping object has the key *key* and ``0`` otherwise.
@@ -86,8 +104,8 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
86104
87105
Exceptions which occur when this calls :meth:`~object.__getitem__`
88106
method are silently ignored.
89-
For proper error handling, use :c:func:`PyMapping_GetOptionalItem` or
90-
:c:func:`PyObject_GetItem()` instead.
107+
For proper error handling, use :c:func:`PyMapping_HasKeyWithError`,
108+
:c:func:`PyMapping_GetOptionalItem` or :c:func:`PyObject_GetItem()` instead.
91109
92110
93111
.. c:function:: int PyMapping_HasKeyString(PyObject *o, const char *key)
@@ -101,7 +119,8 @@ See also :c:func:`PyObject_GetItem`, :c:func:`PyObject_SetItem` and
101119
Exceptions that occur when this calls :meth:`~object.__getitem__`
102120
method or while creating the temporary :class:`str`
103121
object are silently ignored.
104-
For proper error handling, use :c:func:`PyMapping_GetOptionalItemString` or
122+
For proper error handling, use :c:func:`PyMapping_HasKeyStringWithError`,
123+
:c:func:`PyMapping_GetOptionalItemString` or
105124
:c:func:`PyMapping_GetItemString` instead.
106125
107126

Doc/c-api/object.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ Object Protocol
2727
instead of the :func:`repr`.
2828
2929
30+
.. c:function:: int PyObject_HasAttrWithError(PyObject *o, const char *attr_name)
31+
32+
Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.
33+
This is equivalent to the Python expression ``hasattr(o, attr_name)``.
34+
On failure, return ``-1``.
35+
36+
.. versionadded:: 3.13
37+
38+
39+
.. c:function:: int PyObject_HasAttrStringWithError(PyObject *o, const char *attr_name)
40+
41+
This is the same as :c:func:`PyObject_HasAttrWithError`, but *attr_name* is
42+
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
43+
rather than a :c:expr:`PyObject*`.
44+
45+
.. versionadded:: 3.13
46+
47+
3048
.. c:function:: int PyObject_HasAttr(PyObject *o, PyObject *attr_name)
3149
3250
Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise. This
@@ -37,8 +55,8 @@ Object Protocol
3755
3856
Exceptions that occur when this calls :meth:`~object.__getattr__` and
3957
:meth:`~object.__getattribute__` methods are silently ignored.
40-
For proper error handling, use :c:func:`PyObject_GetOptionalAttr` or
41-
:c:func:`PyObject_GetAttr` instead.
58+
For proper error handling, use :c:func:`PyObject_HasAttrWithError`,
59+
:c:func:`PyObject_GetOptionalAttr` or :c:func:`PyObject_GetAttr` instead.
4260
4361
4462
.. c:function:: int PyObject_HasAttrString(PyObject *o, const char *attr_name)
@@ -52,7 +70,8 @@ Object Protocol
5270
Exceptions that occur when this calls :meth:`~object.__getattr__` and
5371
:meth:`~object.__getattribute__` methods or while creating the temporary
5472
:class:`str` object are silently ignored.
55-
For proper error handling, use :c:func:`PyObject_GetOptionalAttrString`
73+
For proper error handling, use :c:func:`PyObject_HasAttrStringWithError`,
74+
:c:func:`PyObject_GetOptionalAttrString`
5675
or :c:func:`PyObject_GetAttrString` instead.
5776
5877

Doc/data/stable_abi.dat

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.13.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,18 @@ New Features
926926
be treated as a failure.
927927
(Contributed by Serhiy Storchaka in :gh:`106307`.)
928928

929+
* Add fixed variants of functions which silently ignore errors:
930+
931+
- :c:func:`PyObject_HasAttrWithError` replaces :c:func:`PyObject_HasAttr`.
932+
- :c:func:`PyObject_HasAttrStringWithError` replaces :c:func:`PyObject_HasAttrString`.
933+
- :c:func:`PyMapping_HasKeyWithError` replaces :c:func:`PyMapping_HasKey`.
934+
- :c:func:`PyMapping_HasKeyStringWithError` replaces :c:func:`PyMapping_HasKeyString`.
935+
936+
New functions return not only ``1`` for true and ``0`` for false, but also
937+
``-1`` for error.
938+
939+
(Contributed by Serhiy Storchaka in :gh:`108511`.)
940+
929941
* If Python is built in :ref:`debug mode <debug-build>` or :option:`with
930942
assertions <--with-assertions>`, :c:func:`PyTuple_SET_ITEM` and
931943
:c:func:`PyList_SET_ITEM` now check the index argument with an assertion.

Include/abstract.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ extern "C" {
5050
5151
This function always succeeds. */
5252

53+
54+
/* Implemented elsewhere:
55+
56+
int PyObject_HasAttrStringWithError(PyObject *o, const char *attr_name);
57+
58+
Returns 1 if object 'o' has the attribute attr_name, and 0 otherwise.
59+
This is equivalent to the Python expression: hasattr(o,attr_name).
60+
Returns -1 on failure. */
61+
62+
63+
/* Implemented elsewhere:
64+
65+
int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name);
66+
67+
Returns 1 if o has the attribute attr_name, and 0 otherwise.
68+
This is equivalent to the Python expression: hasattr(o,attr_name).
69+
Returns -1 on failure. */
70+
71+
5372
/* Implemented elsewhere:
5473
5574
PyObject* PyObject_GetAttr(PyObject *o, PyObject *attr_name);
@@ -821,6 +840,18 @@ PyAPI_FUNC(int) PyMapping_HasKeyString(PyObject *o, const char *key);
821840
This function always succeeds. */
822841
PyAPI_FUNC(int) PyMapping_HasKey(PyObject *o, PyObject *key);
823842

843+
/* Return 1 if the mapping object has the key 'key', and 0 otherwise.
844+
This is equivalent to the Python expression: key in o.
845+
On failure, return -1. */
846+
847+
PyAPI_FUNC(int) PyMapping_HasKeyWithError(PyObject *o, PyObject *key);
848+
849+
/* Return 1 if the mapping object has the key 'key', and 0 otherwise.
850+
This is equivalent to the Python expression: key in o.
851+
On failure, return -1. */
852+
853+
PyAPI_FUNC(int) PyMapping_HasKeyStringWithError(PyObject *o, const char *key);
854+
824855
/* On success, return a list or tuple of the keys in mapping object 'o'.
825856
On failure, return NULL. */
826857
PyAPI_FUNC(PyObject *) PyMapping_Keys(PyObject *o);

Include/object.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,10 @@ PyAPI_FUNC(int) PyObject_GetOptionalAttrString(PyObject *, const char *, PyObjec
394394
PyAPI_FUNC(int) PyObject_SetAttr(PyObject *, PyObject *, PyObject *);
395395
PyAPI_FUNC(int) PyObject_DelAttr(PyObject *v, PyObject *name);
396396
PyAPI_FUNC(int) PyObject_HasAttr(PyObject *, PyObject *);
397+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
398+
PyAPI_FUNC(int) PyObject_HasAttrWithError(PyObject *, PyObject *);
399+
PyAPI_FUNC(int) PyObject_HasAttrStringWithError(PyObject *, const char *);
400+
#endif
397401
PyAPI_FUNC(PyObject *) PyObject_SelfIter(PyObject *);
398402
PyAPI_FUNC(PyObject *) PyObject_GenericGetAttr(PyObject *, PyObject *);
399403
PyAPI_FUNC(int) PyObject_GenericSetAttr(PyObject *, PyObject *, PyObject *);

Lib/test/test_capi/test_abstract.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,34 @@ def test_object_hasattrstring(self):
129129
# CRASHES hasattrstring(obj, NULL)
130130
# CRASHES hasattrstring(NULL, b'a')
131131

132+
def test_object_hasattrwitherror(self):
133+
xhasattr = _testcapi.object_hasattrwitherror
134+
obj = TestObject()
135+
obj.a = 1
136+
setattr(obj, '\U0001f40d', 2)
137+
self.assertTrue(xhasattr(obj, 'a'))
138+
self.assertFalse(xhasattr(obj, 'b'))
139+
self.assertTrue(xhasattr(obj, '\U0001f40d'))
140+
141+
self.assertRaises(RuntimeError, xhasattr, obj, 'evil')
142+
self.assertRaises(TypeError, xhasattr, obj, 1)
143+
# CRASHES xhasattr(obj, NULL)
144+
# CRASHES xhasattr(NULL, 'a')
145+
146+
def test_object_hasattrstringwitherror(self):
147+
hasattrstring = _testcapi.object_hasattrstringwitherror
148+
obj = TestObject()
149+
obj.a = 1
150+
setattr(obj, '\U0001f40d', 2)
151+
self.assertTrue(hasattrstring(obj, b'a'))
152+
self.assertFalse(hasattrstring(obj, b'b'))
153+
self.assertTrue(hasattrstring(obj, '\U0001f40d'.encode()))
154+
155+
self.assertRaises(RuntimeError, hasattrstring, obj, b'evil')
156+
self.assertRaises(UnicodeDecodeError, hasattrstring, obj, b'\xff')
157+
# CRASHES hasattrstring(obj, NULL)
158+
# CRASHES hasattrstring(NULL, b'a')
159+
132160
def test_object_setattr(self):
133161
xsetattr = _testcapi.object_setattr
134162
obj = TestObject()
@@ -339,6 +367,44 @@ def test_mapping_haskeystring(self):
339367
self.assertFalse(haskeystring([], b'a'))
340368
self.assertFalse(haskeystring(NULL, b'a'))
341369

370+
def test_mapping_haskeywitherror(self):
371+
haskey = _testcapi.mapping_haskeywitherror
372+
dct = {'a': 1, '\U0001f40d': 2}
373+
self.assertTrue(haskey(dct, 'a'))
374+
self.assertFalse(haskey(dct, 'b'))
375+
self.assertTrue(haskey(dct, '\U0001f40d'))
376+
377+
dct2 = ProxyGetItem(dct)
378+
self.assertTrue(haskey(dct2, 'a'))
379+
self.assertFalse(haskey(dct2, 'b'))
380+
381+
self.assertTrue(haskey(['a', 'b', 'c'], 1))
382+
383+
self.assertRaises(TypeError, haskey, 42, 'a')
384+
self.assertRaises(TypeError, haskey, {}, []) # unhashable
385+
self.assertRaises(IndexError, haskey, [], 1)
386+
self.assertRaises(TypeError, haskey, [], 'a')
387+
388+
# CRASHES haskey({}, NULL))
389+
# CRASHES haskey(NULL, 'a'))
390+
391+
def test_mapping_haskeystringwitherror(self):
392+
haskeystring = _testcapi.mapping_haskeystringwitherror
393+
dct = {'a': 1, '\U0001f40d': 2}
394+
self.assertTrue(haskeystring(dct, b'a'))
395+
self.assertFalse(haskeystring(dct, b'b'))
396+
self.assertTrue(haskeystring(dct, '\U0001f40d'.encode()))
397+
398+
dct2 = ProxyGetItem(dct)
399+
self.assertTrue(haskeystring(dct2, b'a'))
400+
self.assertFalse(haskeystring(dct2, b'b'))
401+
402+
self.assertRaises(TypeError, haskeystring, 42, b'a')
403+
self.assertRaises(UnicodeDecodeError, haskeystring, {}, b'\xff')
404+
self.assertRaises(SystemError, haskeystring, {}, NULL)
405+
self.assertRaises(TypeError, haskeystring, [], b'a')
406+
# CRASHES haskeystring(NULL, b'a')
407+
342408
def test_object_setitem(self):
343409
setitem = _testcapi.object_setitem
344410
dct = {}

Lib/test/test_stable_abi_ctypes.py

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add functions :c:func:`PyObject_HasAttrWithError`,
2+
:c:func:`PyObject_HasAttrStringWithError`,
3+
:c:func:`PyMapping_HasKeyWithError` and
4+
:c:func:`PyMapping_HasKeyStringWithError`.

Misc/stable_abi.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,3 +2452,11 @@
24522452
added = '3.13'
24532453
[function.PyLong_AsInt]
24542454
added = '3.13'
2455+
[function.PyObject_HasAttrWithError]
2456+
added = '3.13'
2457+
[function.PyObject_HasAttrStringWithError]
2458+
added = '3.13'
2459+
[function.PyMapping_HasKeyWithError]
2460+
added = '3.13'
2461+
[function.PyMapping_HasKeyStringWithError]
2462+
added = '3.13'

0 commit comments

Comments
 (0)