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));