Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2111,7 +2111,7 @@ initialization::

/* Specify sys.path explicitly */
/* If you want to modify the default set of paths, finish
initialization first and then use PySys_GetObject("path") */
initialization first and then use PySys_GetAttrString("path") */
config.module_search_paths_set = 1;
status = PyWideStringList_Append(&config.module_search_paths,
L"/path/to/stdlib");
Expand Down
51 changes: 49 additions & 2 deletions Doc/c-api/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,57 @@ 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)

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 or
if the :mod:`sys` module cannot be found.

If the non-existing object should not be treated as a failure, you can use
:c:func:`PySys_GetOptionalAttr` instead.

.. versionadded:: next

.. c:function:: PyObject *PySys_GetAttrString(const char *name)

This is the same as :c:func:`PySys_GetAttr`, but *name* is
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
rather than a :c:expr:`PyObject*`.

If the non-existing object should not be treated as a failure, you can use
:c:func:`PySys_GetOptionalAttrString` instead.

.. versionadded:: next

.. c:function:: int PySys_GetOptionalAttr(PyObject *name, PyObject **result)

Variant of :c:func:`PySys_GetAttr` which doesn't raise
exception if the object does not exist.

* Set *\*result* to a new :term:`strong reference` to the object and
return ``1`` if the object exists.
* Set *\*result* to ``NULL`` and return ``0`` without setting an exception
if the object does not exist.
* Set an exception, set *\*result* to ``NULL``, and return ``-1``,
if an error occurred.

.. versionadded:: next

.. c:function:: int PySys_GetOptionalAttrString(const char *name, PyObject **result)

This is the same as :c:func:`PySys_GetOptionalAttr`, but *name* is
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
rather than a :c:expr:`PyObject*`.

.. versionadded:: next

.. c:function:: PyObject *PySys_GetObject(const char *name)

Return the object *name* from the :mod:`sys` module or ``NULL`` if it does
not exist, without setting an exception.
Similar to :c:func:`PySys_GetAttrString`, but return a :term:`borrowed
reference` and return ``NULL`` *without* setting exception on failure.

Preserves exception that was set before the call.

.. c:function:: int PySys_SetObject(const char *name, PyObject *v)

Expand Down
4 changes: 4 additions & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Doc/tools/extensions/audit_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from sphinx.util.docutils import SphinxDirective

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Set

from sphinx.application import Sphinx
from sphinx.builders import Builder
Expand All @@ -33,7 +33,7 @@
class AuditEvents:
def __init__(self) -> None:
self.events: dict[str, list[str]] = {}
self.sources: dict[str, list[tuple[str, str]]] = {}
self.sources: dict[str, set[tuple[str, str]]] = {}

def __iter__(self) -> Iterator[tuple[str, list[str], tuple[str, str]]]:
for name, args in self.events.items():
Expand All @@ -47,7 +47,7 @@ def add_event(
self._check_args_match(name, args)
else:
self.events[name] = args
self.sources.setdefault(name, []).append(source)
self.sources.setdefault(name, set()).add(source)

def _check_args_match(self, name: str, args: list[str]) -> None:
current_args = self.events[name]
Expand All @@ -69,11 +69,11 @@ def _check_args_match(self, name: str, args: list[str]) -> None:
return

def id_for(self, name) -> str:
source_count = len(self.sources.get(name, ()))
source_count = len(self.sources.get(name, set()))
name_clean = re.sub(r"\W", "_", name)
return f"audit_event_{name_clean}_{source_count}"

def rows(self) -> Iterator[tuple[str, list[str], list[tuple[str, str]]]]:
def rows(self) -> Iterator[tuple[str, list[str], Set[tuple[str, str]]]]:
for name in sorted(self.events.keys()):
yield name, self.events[name], self.sources[name]

Expand Down Expand Up @@ -218,7 +218,7 @@ def _make_row(
docname: str,
name: str,
args: list[str],
sources: list[tuple[str, str]],
sources: Set[tuple[str, str]],
) -> nodes.row:
row = nodes.row()
name_node = nodes.paragraph("", nodes.Text(name))
Expand All @@ -233,7 +233,7 @@ def _make_row(
row += nodes.entry("", args_node)

backlinks_node = nodes.paragraph()
backlinks = enumerate(sorted(set(sources)), start=1)
backlinks = enumerate(sorted(sources), start=1)
for i, (doc, label) in backlinks:
if isinstance(label, str):
ref = nodes.reference("", f"[{i}]", internal=True)
Expand All @@ -258,7 +258,7 @@ def setup(app: Sphinx):
app.connect("env-purge-doc", audit_events_purge)
app.connect("env-merge-info", audit_events_merge)
return {
"version": "1.0",
"version": "2.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
5 changes: 4 additions & 1 deletion Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ C API changes
New features
------------

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

Porting to Python 3.15
----------------------
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,8 @@ enum _PyAnnotateFormat {
_Py_ANNOTATE_FORMAT_STRING = 4,
};

int _PyObject_SetDict(PyObject *obj, PyObject *value);

#ifdef __cplusplus
}
#endif
Expand Down
5 changes: 0 additions & 5 deletions Include/internal/pycore_sysmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

PyAPI_FUNC(int) _PySys_GetOptionalAttr(PyObject *, PyObject **);
PyAPI_FUNC(int) _PySys_GetOptionalAttrString(const char *, PyObject **);
PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttr(PyObject *);
PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttrString(const char *);

// Export for '_pickle' shared extension
PyAPI_FUNC(size_t) _PySys_GetSizeOf(PyObject *);

Expand Down
6 changes: 6 additions & 0 deletions Include/sysmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
extern "C" {
#endif

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030f0000
PyAPI_FUNC(PyObject *) PySys_GetAttr(PyObject *);
PyAPI_FUNC(PyObject *) PySys_GetAttrString(const char *);
PyAPI_FUNC(int) PySys_GetOptionalAttr(PyObject *, PyObject **);
PyAPI_FUNC(int) PySys_GetOptionalAttrString(const char *, PyObject **);
#endif
PyAPI_FUNC(PyObject *) PySys_GetObject(const char *);
PyAPI_FUNC(int) PySys_SetObject(const char *, PyObject *);

Expand Down
64 changes: 63 additions & 1 deletion Lib/test/test_capi/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,68 @@ class CAPITest(unittest.TestCase):

maxDiff = None

@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
def test_sys_getattr(self):
# Test PySys_GetAttr()
sys_getattr = _testlimitedcapi.sys_getattr

self.assertIs(sys_getattr('stdout'), sys.stdout)
with support.swap_attr(sys, '\U0001f40d', 42):
self.assertEqual(sys_getattr('\U0001f40d'), 42)

with self.assertRaisesRegex(RuntimeError, r'lost sys\.nonexistent'):
sys_getattr('nonexistent')
with self.assertRaisesRegex(RuntimeError, r'lost sys\.\U0001f40d'):
sys_getattr('\U0001f40d')
self.assertRaises(TypeError, sys_getattr, 1)
self.assertRaises(TypeError, sys_getattr, [])
# CRASHES sys_getattr(NULL)

@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
def test_sys_getattrstring(self):
# Test PySys_GetAttrString()
getattrstring = _testlimitedcapi.sys_getattrstring

self.assertIs(getattrstring(b'stdout'), sys.stdout)
with support.swap_attr(sys, '\U0001f40d', 42):
self.assertEqual(getattrstring('\U0001f40d'.encode()), 42)

with self.assertRaisesRegex(RuntimeError, r'lost sys\.nonexistent'):
getattrstring(b'nonexistent')
with self.assertRaisesRegex(RuntimeError, r'lost sys\.\U0001f40d'):
getattrstring('\U0001f40d'.encode())
self.assertRaises(UnicodeDecodeError, getattrstring, b'\xff')
# CRASHES getattrstring(NULL)

@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
def test_sys_getoptionalattr(self):
# Test PySys_GetOptionalAttr()
getoptionalattr = _testlimitedcapi.sys_getoptionalattr

self.assertIs(getoptionalattr('stdout'), sys.stdout)
with support.swap_attr(sys, '\U0001f40d', 42):
self.assertEqual(getoptionalattr('\U0001f40d'), 42)

self.assertIs(getoptionalattr('nonexistent'), AttributeError)
self.assertIs(getoptionalattr('\U0001f40d'), AttributeError)
self.assertRaises(TypeError, getoptionalattr, 1)
self.assertRaises(TypeError, getoptionalattr, [])
# CRASHES getoptionalattr(NULL)

@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
def test_sys_getoptionalattrstring(self):
# Test PySys_GetOptionalAttrString()
getoptionalattrstring = _testlimitedcapi.sys_getoptionalattrstring

self.assertIs(getoptionalattrstring(b'stdout'), sys.stdout)
with support.swap_attr(sys, '\U0001f40d', 42):
self.assertEqual(getoptionalattrstring('\U0001f40d'.encode()), 42)

self.assertIs(getoptionalattrstring(b'nonexistent'), AttributeError)
self.assertIs(getoptionalattrstring('\U0001f40d'.encode()), AttributeError)
self.assertRaises(UnicodeDecodeError, getoptionalattrstring, b'\xff')
# CRASHES getoptionalattrstring(NULL)

@support.cpython_only
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
def test_sys_getobject(self):
Expand All @@ -29,7 +91,7 @@ def test_sys_getobject(self):
with support.swap_attr(sys, '\U0001f40d', 42):
self.assertEqual(getobject('\U0001f40d'.encode()), 42)

self.assertIs(getobject(b'nonexisting'), AttributeError)
self.assertIs(getobject(b'nonexistent'), AttributeError)
with support.catch_unraisable_exception() as cm:
self.assertIs(getobject(b'\xff'), AttributeError)
self.assertEqual(cm.unraisable.exc_type, UnicodeDecodeError)
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_capi/test_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,13 @@ def test_manual_heap_type(self):
ManualHeapType = _testcapi.ManualHeapType
for i in range(100):
self.assertIsInstance(ManualHeapType(), ManualHeapType)

def test_extension_managed_dict_type(self):
ManagedDictType = _testcapi.ManagedDictType
obj = ManagedDictType()
obj.foo = 42
self.assertEqual(obj.foo, 42)
self.assertEqual(obj.__dict__, {'foo': 42})
obj.__dict__ = {'bar': 3}
self.assertEqual(obj.__dict__, {'bar': 3})
self.assertEqual(obj.bar, 3)
46 changes: 46 additions & 0 deletions Lib/test/test_doctest/sample_doctest_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""This is a sample module used for testing doctest.

This module includes various scenarios involving errors.

>>> 2 + 2
5
>>> 1/0
1
"""

def g():
[][0] # line 12

def errors():
"""
>>> 2 + 2
5
>>> 1/0
1
>>> def f():
... 2 + '2'
...
>>> f()
1
>>> g()
1
"""

def syntax_error():
"""
>>> 2+*3
5
"""

__test__ = {
'bad': """
>>> 2 + 2
5
>>> 1/0
1
""",
}

def test_suite():
import doctest
return doctest.DocTestSuite()
Loading
Loading