diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst index 4ab5df4ccccdbb..d70e40e8d2e8d1 100644 --- a/Doc/c-api/sys.rst +++ b/Doc/c-api/sys.rst @@ -258,6 +258,23 @@ These are utility functions that make functionality from the :mod:`sys` module accessible to C code. They all work with the current interpreter thread's :mod:`sys` module's dict, which is contained in the internal thread state structure. +.. c:function:: PyObject *PySys_GetAttr(PyObject *name) + + Return the object *name* from the :mod:`sys` module on success. + Set an exception and return ``NULL`` on + error. + + .. versionadded:: next + + +.. c:function:: PyObject *PySys_GetAttrString(const char *name) + + Similar to :c:func:`PySys_GetAttr`, but *name* is an UTF-8 encoded + string. + + .. versionadded:: next + + .. c:function:: PyObject *PySys_GetObject(const char *name) Return the object *name* from the :mod:`sys` module or ``NULL`` if it does diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index e78754e24e23d8..217efebe97437c 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -3052,3 +3052,9 @@ _Py_c_quot:Py_complex:divisor:: _Py_c_sum:Py_complex::: _Py_c_sum:Py_complex:left:: _Py_c_sum:Py_complex:right:: + +PySys_GetAttr:PyObject*::+1: +PySys_GetAttr:PyObject*:name:0: + +PySys_GetAttrString:PyObject*::+1: +PySys_GetAttrString:const char*:name:: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 2c10d7fefd44ab..f9ce522cc0fe68 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1322,6 +1322,12 @@ New features * Add :c:func:`PyUnstable_IsImmortal` for determining whether an object is :term:`immortal`, for debugging purposes. +* Add :c:func:`PySys_GetAttr` and :c:func:`PySys_GetAttrString` functions to + get an attribute of the :mod:`sys` module. Compared to + :c:func:`PySys_GetObject`, they don't ignore errors and return a + :term:`strong reference`. + (Contributed by Victor Stinner in :gh:`129367`.) + Limited C API changes --------------------- diff --git a/Include/cpython/sysmodule.h b/Include/cpython/sysmodule.h new file mode 100644 index 00000000000000..dd1cf64374b7b0 --- /dev/null +++ b/Include/cpython/sysmodule.h @@ -0,0 +1,6 @@ +#ifndef Py_CPYTHON_SYS_H +# error "this header file must not be included directly" +#endif + +PyAPI_FUNC(PyObject *) PySys_GetAttr(PyObject *name); +PyAPI_FUNC(PyObject *) PySys_GetAttrString(const char *name); diff --git a/Include/sysmodule.h b/Include/sysmodule.h index c1d5f610fe08a5..528c875508a821 100644 --- a/Include/sysmodule.h +++ b/Include/sysmodule.h @@ -21,6 +21,12 @@ Py_DEPRECATED(3.13) PyAPI_FUNC(void) PySys_ResetWarnOptions(void); PyAPI_FUNC(PyObject *) PySys_GetXOptions(void); +#ifndef Py_LIMITED_API +# define Py_CPYTHON_SYS_H +# include "cpython/sysmodule.h" +# undef Py_CPYTHON_SYS_H +#endif + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_capi/test_sys.py b/Lib/test/test_capi/test_sys.py index d3a9b378e7769a..8b74fe4df9ae15 100644 --- a/Lib/test/test_capi/test_sys.py +++ b/Lib/test/test_capi/test_sys.py @@ -4,13 +4,12 @@ from test import support from test.support import import_helper -try: - import _testlimitedcapi -except ImportError: - _testlimitedcapi = None +_testlimitedcapi = import_helper.import_module('_testlimitedcapi') +_testcapi = import_helper.import_module('_testcapi') NULL = None + class CAPITest(unittest.TestCase): # TODO: Test the following functions: # @@ -19,15 +18,18 @@ class CAPITest(unittest.TestCase): maxDiff = None + def check_sys_getattr_common(self, sys_getattr): + self.assertIs(sys_getattr('stdout'), sys.stdout) + + with support.swap_attr(sys, '\U0001f40d', 42): + self.assertEqual(sys_getattr('\U0001f40d'), 42) + @support.cpython_only - @unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module') def test_sys_getobject(self): # Test PySys_GetObject() getobject = _testlimitedcapi.sys_getobject - self.assertIs(getobject(b'stdout'), sys.stdout) - with support.swap_attr(sys, '\U0001f40d', 42): - self.assertEqual(getobject('\U0001f40d'.encode()), 42) + self.check_sys_getattr_common(getobject) self.assertIs(getobject(b'nonexisting'), AttributeError) with support.catch_unraisable_exception() as cm: @@ -37,8 +39,37 @@ def test_sys_getobject(self): "'utf-8' codec can't decode") # CRASHES getobject(NULL) + def check_sys_getattr(self, sys_getattr): + self.check_sys_getattr_common(sys_getattr) + + with self.assertRaises(AttributeError): + sys_getattr(b'nonexisting') + + def test_sys_getattr(self): + # Test PySys_GetAttr() + sys_getattr = _testcapi.PySys_GetAttr + + self.check_sys_getattr(sys_getattr) + + with self.assertRaises(TypeError): + for invalid_name in (123, [], object()): + with self.assertRaises(AttributeError): + sys_getattr(invalid_name) + + # CRASHES sys_getattr(NULL) + + def test_sys_getattrstring(self): + # Test PySys_GetAttr() + sys_getattrstring = _testcapi.PySys_GetAttrString + + self.check_sys_getattr(sys_getattrstring) + + with self.assertRaises(UnicodeDecodeError): + self.assertIs(sys_getattrstring(b'\xff'), AttributeError) + + # CRASHES sys_getattrstring(NULL) + @support.cpython_only - @unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module') def test_sys_setobject(self): # Test PySys_SetObject() setobject = _testlimitedcapi.sys_setobject @@ -70,7 +101,6 @@ def test_sys_setobject(self): # CRASHES setobject(NULL, value) @support.cpython_only - @unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module') def test_sys_getxoptions(self): # Test PySys_GetXOptions() getxoptions = _testlimitedcapi.sys_getxoptions diff --git a/Misc/NEWS.d/next/C_API/2025-01-27-17-40-20.gh-issue-129367.sm8DvA.rst b/Misc/NEWS.d/next/C_API/2025-01-27-17-40-20.gh-issue-129367.sm8DvA.rst new file mode 100644 index 00000000000000..fb948e3f0355d0 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-01-27-17-40-20.gh-issue-129367.sm8DvA.rst @@ -0,0 +1,4 @@ +Add :c:func:`PySys_GetAttr` and :c:func:`PySys_GetAttrString` functions to get +an attribute of the :mod:`sys` module. Compared to :c:func:`PySys_GetObject`, +they don't ignore errors and return a :term:`strong reference`. Patch by Victor +Stinner. diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 6b6a8ae57a5119..8ebc10d5fa3fd6 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -162,7 +162,7 @@ @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c -@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c +@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c _testcapi/sys.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 65ba77596c760e..240dcf638c3ba1 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -61,5 +61,6 @@ int _PyTestCapi_Init_Time(PyObject *module); int _PyTestCapi_Init_Monitoring(PyObject *module); int _PyTestCapi_Init_Object(PyObject *module); int _PyTestCapi_Init_Config(PyObject *mod); +int _PyTestCapi_Init_Sys(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapi/sys.c b/Modules/_testcapi/sys.c new file mode 100644 index 00000000000000..ad0ecab7ecc406 --- /dev/null +++ b/Modules/_testcapi/sys.c @@ -0,0 +1,36 @@ +#include "parts.h" +#include "util.h" + + +static PyObject * +pysys_getattr(PyObject *self, PyObject *name) +{ + NULLABLE(name); + return PySys_GetAttr(name); +} + + +static PyObject * +pysys_getattrstring(PyObject *self, PyObject *arg) +{ + const char *name; + Py_ssize_t size; + if (!PyArg_Parse(arg, "z#", &name, &size)) { + return NULL; + } + + return PySys_GetAttrString(name); +} + + +static PyMethodDef test_methods[] = { + {"PySys_GetAttr", pysys_getattr, METH_O}, + {"PySys_GetAttrString", pysys_getattrstring, METH_O}, + {NULL}, +}; + +int +_PyTestCapi_Init_Sys(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c405a352ed74a1..06a90d567ae3df 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -4401,6 +4401,9 @@ PyInit__testcapi(void) if (_PyTestCapi_Init_Config(m) < 0) { return NULL; } + if (_PyTestCapi_Init_Sys(m) < 0) { + return NULL; + } PyState_AddModule(m, &_testcapimodule); return m; diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index c41235eac356af..62b85d84502f6e 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -127,6 +127,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 0a00df655deefc..778c28b00086c4 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -114,6 +114,9 @@ Source Files + + Source Files + diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 887591a681b25c..504e414ab34696 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -86,6 +86,42 @@ _PySys_GetAttr(PyThreadState *tstate, PyObject *name) return value; } + +PyObject* +PySys_GetAttr(PyObject *name) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + PyObject *sys_dict = interp->sysdict; + if (sys_dict == NULL) { + PyErr_SetString(PyExc_RuntimeError, "lost sys module"); + return NULL; + } + PyObject *value; + if (PyDict_GetItemRef(sys_dict, name, &value) < 0) { + return NULL; + } + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, "sys has no attribute %s", name); + return NULL; + } + return value; +} + + +PyObject* +PySys_GetAttrString(const char *name) +{ + PyObject *name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { + return NULL; + } + + PyObject *value = PySys_GetAttr(name_obj); + Py_DECREF(name_obj); + return value; +} + + static PyObject * _PySys_GetObject(PyInterpreterState *interp, const char *name) { @@ -3150,11 +3186,8 @@ sys_set_flag(PyObject *flags, Py_ssize_t pos, PyObject *value) int _PySys_SetFlagObj(Py_ssize_t pos, PyObject *value) { - PyObject *flags = Py_XNewRef(PySys_GetObject("flags")); + PyObject *flags = PySys_GetAttrString("flags"); if (flags == NULL) { - if (!PyErr_Occurred()) { - PyErr_SetString(PyExc_RuntimeError, "lost sys.flags"); - } return -1; } @@ -3713,16 +3746,15 @@ _PySys_UpdateConfig(PyThreadState *tstate) #undef COPY_WSTR // sys.flags - PyObject *flags = _PySys_GetObject(interp, "flags"); // borrowed ref + PyObject *flags = PySys_GetAttrString("flags"); if (flags == NULL) { - if (!_PyErr_Occurred(tstate)) { - _PyErr_SetString(tstate, PyExc_RuntimeError, "lost sys.flags"); - } return -1; } if (set_flags_from_config(interp, flags) < 0) { + Py_DECREF(flags); return -1; } + Py_DECREF(flags); SET_SYS("dont_write_bytecode", PyBool_FromLong(!config->write_bytecode));