Skip to content

Commit bac3fcb

Browse files
gh-108512: Add and use new replacements for PySys_GetObject() (GH-111035)
Add functions PySys_GetAttr(), PySys_GetAttrString(), PySys_GetOptionalAttr() and PySys_GetOptionalAttrString().
1 parent b265a7d commit bac3fcb

32 files changed

+287
-93
lines changed

Doc/c-api/init_config.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2111,7 +2111,7 @@ initialization::
21112111
21122112
/* Specify sys.path explicitly */
21132113
/* If you want to modify the default set of paths, finish
2114-
initialization first and then use PySys_GetObject("path") */
2114+
initialization first and then use PySys_GetAttrString("path") */
21152115
config.module_search_paths_set = 1;
21162116
status = PyWideStringList_Append(&config.module_search_paths,
21172117
L"/path/to/stdlib");

Doc/c-api/sys.rst

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,57 @@ These are utility functions that make functionality from the :mod:`sys` module
258258
accessible to C code. They all work with the current interpreter thread's
259259
:mod:`sys` module's dict, which is contained in the internal thread state structure.
260260
261+
.. c:function:: PyObject *PySys_GetAttr(PyObject *name)
262+
263+
Get the attribute *name* of the :mod:`sys` module.
264+
Return a :term:`strong reference`.
265+
Raise :exc:`RuntimeError` and return ``NULL`` if it does not exist or
266+
if the :mod:`sys` module cannot be found.
267+
268+
If the non-existing object should not be treated as a failure, you can use
269+
:c:func:`PySys_GetOptionalAttr` instead.
270+
271+
.. versionadded:: next
272+
273+
.. c:function:: PyObject *PySys_GetAttrString(const char *name)
274+
275+
This is the same as :c:func:`PySys_GetAttr`, but *name* is
276+
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
277+
rather than a :c:expr:`PyObject*`.
278+
279+
If the non-existing object should not be treated as a failure, you can use
280+
:c:func:`PySys_GetOptionalAttrString` instead.
281+
282+
.. versionadded:: next
283+
284+
.. c:function:: int PySys_GetOptionalAttr(PyObject *name, PyObject **result)
285+
286+
Variant of :c:func:`PySys_GetAttr` which doesn't raise
287+
exception if the object does not exist.
288+
289+
* Set *\*result* to a new :term:`strong reference` to the object and
290+
return ``1`` if the object exists.
291+
* Set *\*result* to ``NULL`` and return ``0`` without setting an exception
292+
if the object does not exist.
293+
* Set an exception, set *\*result* to ``NULL``, and return ``-1``,
294+
if an error occurred.
295+
296+
.. versionadded:: next
297+
298+
.. c:function:: int PySys_GetOptionalAttrString(const char *name, PyObject **result)
299+
300+
This is the same as :c:func:`PySys_GetOptionalAttr`, but *name* is
301+
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
302+
rather than a :c:expr:`PyObject*`.
303+
304+
.. versionadded:: next
305+
261306
.. c:function:: PyObject *PySys_GetObject(const char *name)
262307
263-
Return the object *name* from the :mod:`sys` module or ``NULL`` if it does
264-
not exist, without setting an exception.
308+
Similar to :c:func:`PySys_GetAttrString`, but return a :term:`borrowed
309+
reference` and return ``NULL`` *without* setting exception on failure.
310+
311+
Preserves exception that was set before the call.
265312
266313
.. c:function:: int PySys_SetObject(const char *name, PyObject *v)
267314

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.15.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ C API changes
213213
New features
214214
------------
215215

216-
* TODO
216+
* Add :c:func:`PySys_GetAttr`, :c:func:`PySys_GetAttrString`,
217+
:c:func:`PySys_GetOptionalAttr`, and :c:func:`PySys_GetOptionalAttrString`
218+
functions as replacements for :c:func:`PySys_GetObject`.
219+
(Contributed by Serhiy Storchaka in :gh:`108512`.)
217220

218221
Porting to Python 3.15
219222
----------------------

Include/internal/pycore_sysmodule.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11-
PyAPI_FUNC(int) _PySys_GetOptionalAttr(PyObject *, PyObject **);
12-
PyAPI_FUNC(int) _PySys_GetOptionalAttrString(const char *, PyObject **);
13-
PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttr(PyObject *);
14-
PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttrString(const char *);
15-
1611
// Export for '_pickle' shared extension
1712
PyAPI_FUNC(size_t) _PySys_GetSizeOf(PyObject *);
1813

Include/sysmodule.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
extern "C" {
55
#endif
66

7+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030f0000
8+
PyAPI_FUNC(PyObject *) PySys_GetAttr(PyObject *);
9+
PyAPI_FUNC(PyObject *) PySys_GetAttrString(const char *);
10+
PyAPI_FUNC(int) PySys_GetOptionalAttr(PyObject *, PyObject **);
11+
PyAPI_FUNC(int) PySys_GetOptionalAttrString(const char *, PyObject **);
12+
#endif
713
PyAPI_FUNC(PyObject *) PySys_GetObject(const char *);
814
PyAPI_FUNC(int) PySys_SetObject(const char *, PyObject *);
915

Lib/test/test_capi/test_sys.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,68 @@ class CAPITest(unittest.TestCase):
1919

2020
maxDiff = None
2121

22+
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
23+
def test_sys_getattr(self):
24+
# Test PySys_GetAttr()
25+
sys_getattr = _testlimitedcapi.sys_getattr
26+
27+
self.assertIs(sys_getattr('stdout'), sys.stdout)
28+
with support.swap_attr(sys, '\U0001f40d', 42):
29+
self.assertEqual(sys_getattr('\U0001f40d'), 42)
30+
31+
with self.assertRaisesRegex(RuntimeError, r'lost sys\.nonexistent'):
32+
sys_getattr('nonexistent')
33+
with self.assertRaisesRegex(RuntimeError, r'lost sys\.\U0001f40d'):
34+
sys_getattr('\U0001f40d')
35+
self.assertRaises(TypeError, sys_getattr, 1)
36+
self.assertRaises(TypeError, sys_getattr, [])
37+
# CRASHES sys_getattr(NULL)
38+
39+
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
40+
def test_sys_getattrstring(self):
41+
# Test PySys_GetAttrString()
42+
getattrstring = _testlimitedcapi.sys_getattrstring
43+
44+
self.assertIs(getattrstring(b'stdout'), sys.stdout)
45+
with support.swap_attr(sys, '\U0001f40d', 42):
46+
self.assertEqual(getattrstring('\U0001f40d'.encode()), 42)
47+
48+
with self.assertRaisesRegex(RuntimeError, r'lost sys\.nonexistent'):
49+
getattrstring(b'nonexistent')
50+
with self.assertRaisesRegex(RuntimeError, r'lost sys\.\U0001f40d'):
51+
getattrstring('\U0001f40d'.encode())
52+
self.assertRaises(UnicodeDecodeError, getattrstring, b'\xff')
53+
# CRASHES getattrstring(NULL)
54+
55+
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
56+
def test_sys_getoptionalattr(self):
57+
# Test PySys_GetOptionalAttr()
58+
getoptionalattr = _testlimitedcapi.sys_getoptionalattr
59+
60+
self.assertIs(getoptionalattr('stdout'), sys.stdout)
61+
with support.swap_attr(sys, '\U0001f40d', 42):
62+
self.assertEqual(getoptionalattr('\U0001f40d'), 42)
63+
64+
self.assertIs(getoptionalattr('nonexistent'), AttributeError)
65+
self.assertIs(getoptionalattr('\U0001f40d'), AttributeError)
66+
self.assertRaises(TypeError, getoptionalattr, 1)
67+
self.assertRaises(TypeError, getoptionalattr, [])
68+
# CRASHES getoptionalattr(NULL)
69+
70+
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
71+
def test_sys_getoptionalattrstring(self):
72+
# Test PySys_GetOptionalAttrString()
73+
getoptionalattrstring = _testlimitedcapi.sys_getoptionalattrstring
74+
75+
self.assertIs(getoptionalattrstring(b'stdout'), sys.stdout)
76+
with support.swap_attr(sys, '\U0001f40d', 42):
77+
self.assertEqual(getoptionalattrstring('\U0001f40d'.encode()), 42)
78+
79+
self.assertIs(getoptionalattrstring(b'nonexistent'), AttributeError)
80+
self.assertIs(getoptionalattrstring('\U0001f40d'.encode()), AttributeError)
81+
self.assertRaises(UnicodeDecodeError, getoptionalattrstring, b'\xff')
82+
# CRASHES getoptionalattrstring(NULL)
83+
2284
@support.cpython_only
2385
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
2486
def test_sys_getobject(self):
@@ -29,7 +91,7 @@ def test_sys_getobject(self):
2991
with support.swap_attr(sys, '\U0001f40d', 42):
3092
self.assertEqual(getobject('\U0001f40d'.encode()), 42)
3193

32-
self.assertIs(getobject(b'nonexisting'), AttributeError)
94+
self.assertIs(getobject(b'nonexistent'), AttributeError)
3395
with support.catch_unraisable_exception() as cm:
3496
self.assertIs(getobject(b'\xff'), AttributeError)
3597
self.assertEqual(cm.unraisable.exc_type, UnicodeDecodeError)

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add functions :c:func:`PySys_GetAttr`, :c:func:`PySys_GetAttrString`,
2+
:c:func:`PySys_GetOptionalAttr` and :c:func:`PySys_GetOptionalAttrString`.

Misc/stable_abi.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,3 +2575,11 @@
25752575
added = '3.14'
25762576
[function.Py_PACK_VERSION]
25772577
added = '3.14'
2578+
[function.PySys_GetAttr]
2579+
added = '3.15'
2580+
[function.PySys_GetAttrString]
2581+
added = '3.15'
2582+
[function.PySys_GetOptionalAttr]
2583+
added = '3.15'
2584+
[function.PySys_GetOptionalAttrString]
2585+
added = '3.15'

Modules/_cursesmodule.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ static const char PyCursesVersion[] = "2.2";
108108
#include "pycore_capsule.h" // _PyCapsule_SetTraverse()
109109
#include "pycore_long.h" // _PyLong_GetZero()
110110
#include "pycore_structseq.h" // _PyStructSequence_NewType()
111-
#include "pycore_sysmodule.h" // _PySys_GetOptionalAttrString()
112111
#include "pycore_fileutils.h" // _Py_set_inheritable
113112

114113
#ifdef __hpux
@@ -3847,7 +3846,7 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd)
38473846
if (fd == -1) {
38483847
PyObject* sys_stdout;
38493848

3850-
if (_PySys_GetOptionalAttrString("stdout", &sys_stdout) < 0) {
3849+
if (PySys_GetOptionalAttrString("stdout", &sys_stdout) < 0) {
38513850
return NULL;
38523851
}
38533852

Modules/_lsprof.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ _lsprof_Profiler_enable_impl(ProfilerObject *self, int subcalls,
782782
return NULL;
783783
}
784784

785-
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
785+
PyObject* monitoring = PySys_GetAttrString("monitoring");
786786
if (!monitoring) {
787787
return NULL;
788788
}
@@ -864,7 +864,7 @@ _lsprof_Profiler_disable_impl(ProfilerObject *self)
864864
}
865865
if (self->flags & POF_ENABLED) {
866866
PyObject* result = NULL;
867-
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
867+
PyObject* monitoring = PySys_GetAttrString("monitoring");
868868

869869
if (!monitoring) {
870870
return NULL;
@@ -983,7 +983,7 @@ profiler_init_impl(ProfilerObject *self, PyObject *timer, double timeunit,
983983
Py_XSETREF(self->externalTimer, Py_XNewRef(timer));
984984
self->tool_id = PY_MONITORING_PROFILER_ID;
985985

986-
PyObject* monitoring = PyImport_ImportModuleAttrString("sys", "monitoring");
986+
PyObject* monitoring = PySys_GetAttrString("monitoring");
987987
if (!monitoring) {
988988
return -1;
989989
}

Modules/_pickle.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1915,7 +1915,7 @@ whichmodule(PickleState *st, PyObject *global, PyObject *global_name, PyObject *
19151915
__module__ can be None. If it is so, then search sys.modules for
19161916
the module of global. */
19171917
Py_CLEAR(module_name);
1918-
modules = _PySys_GetRequiredAttr(&_Py_ID(modules));
1918+
modules = PySys_GetAttr(&_Py_ID(modules));
19191919
if (modules == NULL) {
19201920
return NULL;
19211921
}

Modules/_testlimitedcapi/sys.c

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,76 @@
1+
#include "pyconfig.h" // Py_GIL_DISABLED
2+
// Need limited C API version 3.15 for PySys_GetAttr() etc
3+
#if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API)
4+
# define Py_LIMITED_API 0x030f0000
5+
#endif
16
#include "parts.h"
27
#include "util.h"
38

49

10+
static PyObject *
11+
sys_getattr(PyObject *Py_UNUSED(module), PyObject *name)
12+
{
13+
NULLABLE(name);
14+
return PySys_GetAttr(name);
15+
}
16+
17+
static PyObject *
18+
sys_getattrstring(PyObject *Py_UNUSED(module), PyObject *arg)
19+
{
20+
const char *name;
21+
Py_ssize_t size;
22+
if (!PyArg_Parse(arg, "z#", &name, &size)) {
23+
return NULL;
24+
}
25+
return PySys_GetAttrString(name);
26+
}
27+
28+
static PyObject *
29+
sys_getoptionalattr(PyObject *Py_UNUSED(module), PyObject *name)
30+
{
31+
PyObject *value = UNINITIALIZED_PTR;
32+
NULLABLE(name);
33+
34+
switch (PySys_GetOptionalAttr(name, &value)) {
35+
case -1:
36+
assert(value == NULL);
37+
assert(PyErr_Occurred());
38+
return NULL;
39+
case 0:
40+
assert(value == NULL);
41+
return Py_NewRef(PyExc_AttributeError);
42+
case 1:
43+
return value;
44+
default:
45+
Py_FatalError("PySys_GetOptionalAttr() returned invalid code");
46+
}
47+
}
48+
49+
static PyObject *
50+
sys_getoptionalattrstring(PyObject *Py_UNUSED(module), PyObject *arg)
51+
{
52+
PyObject *value = UNINITIALIZED_PTR;
53+
const char *name;
54+
Py_ssize_t size;
55+
if (!PyArg_Parse(arg, "z#", &name, &size)) {
56+
return NULL;
57+
}
58+
59+
switch (PySys_GetOptionalAttrString(name, &value)) {
60+
case -1:
61+
assert(value == NULL);
62+
assert(PyErr_Occurred());
63+
return NULL;
64+
case 0:
65+
assert(value == NULL);
66+
return Py_NewRef(PyExc_AttributeError);
67+
case 1:
68+
return value;
69+
default:
70+
Py_FatalError("PySys_GetOptionalAttrString() returned invalid code");
71+
}
72+
}
73+
574
static PyObject *
675
sys_getobject(PyObject *Py_UNUSED(module), PyObject *arg)
776
{
@@ -39,6 +108,10 @@ sys_getxoptions(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(ignored))
39108

40109

41110
static PyMethodDef test_methods[] = {
111+
{"sys_getattr", sys_getattr, METH_O},
112+
{"sys_getattrstring", sys_getattrstring, METH_O},
113+
{"sys_getoptionalattr", sys_getoptionalattr, METH_O},
114+
{"sys_getoptionalattrstring", sys_getoptionalattrstring, METH_O},
42115
{"sys_getobject", sys_getobject, METH_O},
43116
{"sys_setobject", sys_setobject, METH_VARARGS},
44117
{"sys_getxoptions", sys_getxoptions, METH_NOARGS},

Modules/_threadmodule.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount()
1111
#include "pycore_pylifecycle.h"
1212
#include "pycore_pystate.h" // _PyThreadState_SetCurrent()
13-
#include "pycore_sysmodule.h" // _PySys_GetOptionalAttr()
1413
#include "pycore_time.h" // _PyTime_FromSeconds()
1514
#include "pycore_weakref.h" // _PyWeakref_GET_REF()
1615

@@ -2290,7 +2289,7 @@ thread_excepthook(PyObject *module, PyObject *args)
22902289
PyObject *thread = PyStructSequence_GET_ITEM(args, 3);
22912290

22922291
PyObject *file;
2293-
if (_PySys_GetOptionalAttr( &_Py_ID(stderr), &file) < 0) {
2292+
if (PySys_GetOptionalAttr( &_Py_ID(stderr), &file) < 0) {
22942293
return NULL;
22952294
}
22962295
if (file == NULL || file == Py_None) {

0 commit comments

Comments
 (0)