-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
gh-108512: Add and use new replacements for PySys_GetObject() #111035
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
base: main
Are you sure you want to change the base?
gh-108512: Add and use new replacements for PySys_GetObject() #111035
Conversation
Add functions PySys_GetAttr(), PySys_GetAttrString(), PySys_GetOptionalAttr() and PySys_GetOptionalAttrString().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to leave changes to use these changes aside, in a separated PR, so we can focus only on the API, doc and tests on the new functions?
The function is called "GetAttr" but in the doc, you wrote "if the object exists". IMO "if the attribute exists" is more appropriate.
Is it required to add these new functions to the stable ABI in Python 3.13? Can we wait one Python release to see how it does, before add them to the stable ABI?
Would it be possible to add tests?
If the object exists, set *\*result* to a new :term:`strong reference` | ||
to the object and return ``1``. | ||
If the object does not exist, set *\*result* to ``NULL`` and return ``0``, | ||
without setting an exception. | ||
If other error occurred, set an exception, set *\*result* to ``NULL`` and | ||
return ``-1``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to use a list and start with the return value, so it's easier to see the 3 cases:
If the object exists, set *\*result* to a new :term:`strong reference` | |
to the object and return ``1``. | |
If the object does not exist, set *\*result* to ``NULL`` and return ``0``, | |
without setting an exception. | |
If other error occurred, set an exception, set *\*result* to ``NULL`` and | |
return ``-1``. | |
* Return ``1`` and set *\*result* to a new :term:`strong reference` | |
to the object if the attribute exists. | |
* Return ``0`` without setting an exception and set *\*result* to ``NULL`` | |
if the attribute does not exist. | |
* Return ``-1``, set an exception and set *\*result* to ``NULL`` | |
if an error occurred. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Return and then set exception and variable" looks like a wrong sequence to me. It cannot do anything after returning.
Co-authored-by: Victor Stinner <vstinner@python.org>
Lib/test/test_capi/test_misc.py
Outdated
with support.swap_attr(sys, '\U0001f40d', 42): | ||
self.assertEqual(sys_getattr('\U0001f40d'), 42) | ||
|
||
with self.assertRaisesRegex(RuntimeError, r'lost sys\.nonexisting'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message is surprising. sys has no attribute "nonexisting". It's not "lost", it simply doesn't exist.
I would prefer to always raise AttributeError with a message like "module 'sys' has no attribute 'x'", similar than in Python:
>>> import sys
>>> sys.x
AttributeError: module 'sys' has no attribute 'x'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It leaves no use cases for PySys_GetAttr()
. They all can be replaced with PySys_GetOptionalAttr()
followed by PyErr_SetString(PyExc_RuntimeError,)
.
If you leave PySys_GetAttr()
, you will always use it with PyErr_ExceptionMatches(PyExc_AttributeError)
followed by PyErr_SetString(PyExc_RuntimeError,)
.
Lib/test/test_capi/test_misc.py
Outdated
# CRASHES sys_getattr(NULL) | ||
|
||
def test_sys_getoptionalattr(self): | ||
sys_getattr = _testcapi.sys_getoptionalattr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's surprising that in all tests, the function is called "sys_getattr", whereas here you test PySys_GetOptionalAttr(), not PySys_GetAttr().
I suggest to rename the variable t o"sys_getoptionalattr", or even PySys_GetOptionalAttr() since this is a C API test. It would be more explicit to use the name of the C API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it was exactly what I wrote initially, but in last minute I replaced all names with the same name to make reading easy. But if you think that it does not help, I'll restore previous names.
Lib/test/test_capi/test_misc.py
Outdated
self.assertEqual(sys_getattr('\U0001f40d'.encode()), 42) | ||
|
||
self.assertIs(sys_getattr(b'nonexisting'), AttributeError) | ||
self.assertRaises(UnicodeDecodeError, sys_getattr, b'\xff') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a UnicodeDecodeError to test_sys_getattr() as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, PySys_GetAttr()
does not raise UnicodeDecodeError. The wrapper does, but we do not test PyArg_Parse()
here.
Modules/_testcapimodule.c
Outdated
|
||
switch (PySys_GetOptionalAttr(name, &value)) { | ||
case -1: | ||
assert(value == NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest adding: assert(PyErr_Occurred());
. Same remark in sys_getoptionalattrstring().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't there a check that each function that returns NULL should also set an exception? I relied on this in all other tests.
Modules/_testcapimodule.c
Outdated
return value; | ||
default: | ||
Py_FatalError("PySys_GetOptionalAttr() returned invalid code"); | ||
Py_UNREACHABLE(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should not be needed, Py_FatalError() is annotated with _Py_NO_RETURN. Same remark in sys_getoptionalattrstring().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, removing.
Modules/_testcapimodule.c
Outdated
} | ||
if (result == NULL) { | ||
result = PyExc_AttributeError; | ||
Py_INCREF(PyExc_AttributeError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this code path. The function must return NULL and raise an exception if the attribute does not exist. This code path must never be reached according to the API doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it exactly to test that it never happens. But perhaps it can be removed.
It is easy to see the effect of these changes if they are in a single PR. Also, it replaces old private functions with new public API that is impossible if they are still used. If you prefer, I will split this PR into two parts after the development of the new API is complete, immediately before merging.
These are the words used in the description of the existing function
What exactly do you propose? Isn't it a point that we can suggest them as replacements for
Good point, I forgot about this. Done. |
Well, trying to get |
I'm asking if we should only add the API to Include/cpython/ in Python 3.13, and wait for Python 3.14 to add it to the limited API. Just in case if something goes wrong, if the API changes when we notice issues. If it lands directly in the stable ABI, we can no longer change it, it's too late. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the exception, I see two options:
-
We consider that we get an object from a namespace, similar to LOAD_GLOBAL, and so NameError should be raised. IMO the function should be called PySys_GetVar() in this case. Example: https://docs.python.org/dev/c-api/frame.html#c.PyFrame_GetVar raises NameError.
-
Or we consider that we are getting an attribute, PySys_GetAttr() name is good, and AttributeError should be raised in this case.
For me, RuntimeError is just meaningless and it should be avoided. RuntimeError means everything and nothing: "something failed". Thanks Python...
In Python, usually I consider that the sys module is an object and I get sys attributes with getattr(sys, "stdout")
which raises AttributeError.
In Python, I'm not even sure how to treat sys as a namespace. sys.__dict__['stdout']
raises KeyError, not NameError.
Many of existing uses of |
.. c:function:: PyObject *PySys_GetAttr(PyObject *name) | ||
|
||
Get the attribute *name* of the :mod:`sys` module. Return a :term:`strong reference`. | ||
Raise :exc:`RuntimeError` and return ``NULL`` if it does not exist. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to raise AttributeError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we would need a new pair of functions that raise RuntimeError to use in the CPython code. Or you can add a pair of functions that raise AttributeError later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we would need a new pair of functions that raise RuntimeError to use in the CPython code.
Would you mind to elaborate why RuntimeError is needed by CPython?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Status quo. And it looks to me it is justified by the purpose of RuntimeError: "Raised when an error is detected that doesn’t fall in any of the other categories."
If you want to change RuntimeError's in the existing code to something other, you should justify that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my implementation #129369, PySys_SetAttr()
raises RuntimeError
if the sys
module cannot be retrieved. But it raises AttributeError
if the attribute doesn't exist.
PySys_GetObject()
ignores all errors and returns NULL
on error or if the attribute doesn't exist. So I'm confused, which function using PySys_GetObject()
currently rely on the RuntimeError
? Do you have an example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About a half of functions that use PySys_GetObject()
or _PySys_GetAttr()
raise RuntimeError if they return NULL. See my statistics in capi-workgroup/decisions#54. Look at usages of PySys_GetAttr()
and PySys_GetAttrString()
in this PR.
Add functions PySys_GetAttr(), PySys_GetAttrString(), PySys_GetOptionalAttr() and PySys_GetOptionalAttrString().
📚 Documentation preview 📚: https://cpython-previews--111035.org.readthedocs.build/