Skip to content

Commit bc77eff

Browse files
authored
bpo-33042: Fix pre-initialization sys module configuration (GH-6157)
- new test case for pre-initialization of sys.warnoptions and sys._xoptions - restored ability to call these APIs prior to Py_Initialize - updated the docs for the affected APIs to make it clear they can be called before Py_Initialize - also enhanced the existing embedding test cases to check for expected settings in the sys module
1 parent d02ac25 commit bc77eff

File tree

7 files changed

+296
-12
lines changed

7 files changed

+296
-12
lines changed

Doc/c-api/init.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ The following functions can be safely called before Python is initialized:
3131
* :c:func:`Py_SetProgramName`
3232
* :c:func:`Py_SetPythonHome`
3333
* :c:func:`Py_SetStandardStreamEncoding`
34+
* :c:func:`PySys_AddWarnOption`
35+
* :c:func:`PySys_AddXOption`
36+
* :c:func:`PySys_ResetWarnOptions`
3437

3538
* Informative functions:
3639

Doc/c-api/sys.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,24 @@ accessible to C code. They all work with the current interpreter thread's
205205
206206
.. c:function:: void PySys_ResetWarnOptions()
207207
208-
Reset :data:`sys.warnoptions` to an empty list.
208+
Reset :data:`sys.warnoptions` to an empty list. This function may be
209+
called prior to :c:func:`Py_Initialize`.
209210
210211
.. c:function:: void PySys_AddWarnOption(const wchar_t *s)
211212
212-
Append *s* to :data:`sys.warnoptions`.
213+
Append *s* to :data:`sys.warnoptions`. This function must be called prior
214+
to :c:func:`Py_Initialize` in order to affect the warnings filter list.
213215
214216
.. c:function:: void PySys_AddWarnOptionUnicode(PyObject *unicode)
215217
216218
Append *unicode* to :data:`sys.warnoptions`.
217219
220+
Note: this function is not currently usable from outside the CPython
221+
implementation, as it must be called prior to the implicit import of
222+
:mod:`warnings` in :c:func:`Py_Initialize` to be effective, but can't be
223+
called until enough of the runtime has been initialized to permit the
224+
creation of Unicode objects.
225+
218226
.. c:function:: void PySys_SetPath(const wchar_t *path)
219227
220228
Set :data:`sys.path` to a list object of paths found in *path* which should
@@ -260,7 +268,8 @@ accessible to C code. They all work with the current interpreter thread's
260268
.. c:function:: void PySys_AddXOption(const wchar_t *s)
261269
262270
Parse *s* as a set of :option:`-X` options and add them to the current
263-
options mapping as returned by :c:func:`PySys_GetXOptions`.
271+
options mapping as returned by :c:func:`PySys_GetXOptions`. This function
272+
may be called prior to :c:func:`Py_Initialize`.
264273
265274
.. versionadded:: 3.2
266275

Doc/whatsnew/3.7.rst

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,14 @@ Build and C API Changes
951951
second argument is *NULL* and the :c:type:`wchar_t*` string contains null
952952
characters. (Contributed by Serhiy Storchaka in :issue:`30708`.)
953953

954+
- Changes to the startup sequence and the management of dynamic memory
955+
allocators mean that the long documented requirement to call
956+
:c:func:`Py_Initialize` before calling most C API functions is now
957+
relied on more heavily, and failing to abide by it may lead to segfaults in
958+
embedding applications. See the :ref:`porting-to-python-37` section in this
959+
document and the :ref:`pre-init-safe` section in the C API documentation
960+
for more details.
961+
954962

955963
Other CPython Implementation Changes
956964
====================================
@@ -1098,6 +1106,7 @@ API and Feature Removals
10981106
``asyncio._overlapped``. Replace ``from asyncio import selectors`` with
10991107
``import selectors`` for example.
11001108

1109+
.. _porting-to-python-37:
11011110

11021111
Porting to Python 3.7
11031112
=====================
@@ -1282,14 +1291,24 @@ Other CPython implementation changes
12821291
------------------------------------
12831292

12841293
* In preparation for potential future changes to the public CPython runtime
1285-
initialization API (see :pep:`432` for details), CPython's internal startup
1294+
initialization API (see :pep:`432` for an initial, but somewhat outdated,
1295+
draft), CPython's internal startup
12861296
and configuration management logic has been significantly refactored. While
12871297
these updates are intended to be entirely transparent to both embedding
12881298
applications and users of the regular CPython CLI, they're being mentioned
12891299
here as the refactoring changes the internal order of various operations
12901300
during interpreter startup, and hence may uncover previously latent defects,
12911301
either in embedding applications, or in CPython itself.
1292-
(Contributed by Nick Coghlan and Eric Snow as part of :issue:`22257`.)
1302+
(Initially contributed by Nick Coghlan and Eric Snow as part of
1303+
:issue:`22257`, and further updated by Nick, Eric, and Victor Stinner in a
1304+
number of other issues). Some known details affected:
1305+
1306+
* :c:func:`PySys_AddWarnOptionUnicode` is not currently usable by embedding
1307+
applications due to the requirement to create a Unicode object prior to
1308+
calling `Py_Initialize`. Use :c:func:`PySys_AddWarnOption` instead.
1309+
* warnings filters added by an embedding application with
1310+
:c:func:`PySys_AddWarnOption` should now more consistently take precedence
1311+
over the default filters set by the interpreter
12931312

12941313
* Due to changes in the way the default warnings filters are configured,
12951314
setting :c:data:`Py_BytesWarningFlag` to a value greater than one is no longer

Lib/test/test_embed.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def run_embedded_interpreter(self, *args, env=None):
5151
if p.returncode != 0 and support.verbose:
5252
print(f"--- {cmd} failed ---")
5353
print(f"stdout:\n{out}")
54-
print(f"stderr:\n{out}")
54+
print(f"stderr:\n{err}")
5555
print(f"------")
5656

5757
self.assertEqual(p.returncode, 0,
@@ -83,7 +83,7 @@ def run_repeated_init_and_subinterpreters(self):
8383
for line in out.splitlines():
8484
if line == "--- Pass {} ---".format(numloops):
8585
self.assertEqual(len(current_run), 0)
86-
if support.verbose:
86+
if support.verbose > 1:
8787
print(line)
8888
numloops += 1
8989
continue
@@ -96,7 +96,7 @@ def run_repeated_init_and_subinterpreters(self):
9696
# Parse the line from the loop. The first line is the main
9797
# interpreter and the 3 afterward are subinterpreters.
9898
interp = Interp(*match.groups())
99-
if support.verbose:
99+
if support.verbose > 1:
100100
print(interp)
101101
self.assertTrue(interp.interp)
102102
self.assertTrue(interp.tstate)
@@ -190,12 +190,33 @@ def test_forced_io_encoding(self):
190190

191191
def test_pre_initialization_api(self):
192192
"""
193-
Checks the few parts of the C-API that work before the runtine
193+
Checks some key parts of the C-API that need to work before the runtine
194194
is initialized (via Py_Initialize()).
195195
"""
196196
env = dict(os.environ, PYTHONPATH=os.pathsep.join(sys.path))
197197
out, err = self.run_embedded_interpreter("pre_initialization_api", env=env)
198-
self.assertEqual(out, '')
198+
if sys.platform == "win32":
199+
expected_path = self.test_exe
200+
else:
201+
expected_path = os.path.join(os.getcwd(), "spam")
202+
expected_output = f"sys.executable: {expected_path}\n"
203+
self.assertIn(expected_output, out)
204+
self.assertEqual(err, '')
205+
206+
def test_pre_initialization_sys_options(self):
207+
"""
208+
Checks that sys.warnoptions and sys._xoptions can be set before the
209+
runtime is initialized (otherwise they won't be effective).
210+
"""
211+
env = dict(PYTHONPATH=os.pathsep.join(sys.path))
212+
out, err = self.run_embedded_interpreter(
213+
"pre_initialization_sys_options", env=env)
214+
expected_output = (
215+
"sys.warnoptions: ['once', 'module', 'default']\n"
216+
"sys._xoptions: {'not_an_option': '1', 'also_not_an_option': '2'}\n"
217+
"warnings.filters[:3]: ['default', 'module', 'once']\n"
218+
)
219+
self.assertIn(expected_output, out)
199220
self.assertEqual(err, '')
200221

201222
def test_bpo20891(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Embedding applications may once again call PySys_ResetWarnOptions,
2+
PySys_AddWarnOption, and PySys_AddXOption prior to calling Py_Initialize.

Programs/_testembed.c

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "pythread.h"
33
#include <inttypes.h>
44
#include <stdio.h>
5+
#include <wchar.h>
56

67
/*********************************************************
78
* Embedded interpreter tests that need a custom exe
@@ -130,23 +131,89 @@ static int test_forced_io_encoding(void)
130131
* Test parts of the C-API that work before initialization
131132
*********************************************************/
132133

134+
/* The pre-initialization tests tend to break by segfaulting, so explicitly
135+
* flushed progress messages make the broken API easier to find when they fail.
136+
*/
137+
#define _Py_EMBED_PREINIT_CHECK(msg) \
138+
do {printf(msg); fflush(stdout);} while (0);
139+
133140
static int test_pre_initialization_api(void)
134141
{
135142
/* Leading "./" ensures getpath.c can still find the standard library */
143+
_Py_EMBED_PREINIT_CHECK("Checking Py_DecodeLocale\n");
136144
wchar_t *program = Py_DecodeLocale("./spam", NULL);
137145
if (program == NULL) {
138146
fprintf(stderr, "Fatal error: cannot decode program name\n");
139147
return 1;
140148
}
149+
_Py_EMBED_PREINIT_CHECK("Checking Py_SetProgramName\n");
141150
Py_SetProgramName(program);
142151

152+
_Py_EMBED_PREINIT_CHECK("Initializing interpreter\n");
143153
Py_Initialize();
154+
_Py_EMBED_PREINIT_CHECK("Check sys module contents\n");
155+
PyRun_SimpleString("import sys; "
156+
"print('sys.executable:', sys.executable)");
157+
_Py_EMBED_PREINIT_CHECK("Finalizing interpreter\n");
144158
Py_Finalize();
145159

160+
_Py_EMBED_PREINIT_CHECK("Freeing memory allocated by Py_DecodeLocale\n");
146161
PyMem_RawFree(program);
147162
return 0;
148163
}
149164

165+
166+
/* bpo-33042: Ensure embedding apps can predefine sys module options */
167+
static int test_pre_initialization_sys_options(void)
168+
{
169+
/* We allocate a couple of the option dynamically, and then delete
170+
* them before calling Py_Initialize. This ensures the interpreter isn't
171+
* relying on the caller to keep the passed in strings alive.
172+
*/
173+
wchar_t *static_warnoption = L"once";
174+
wchar_t *static_xoption = L"also_not_an_option=2";
175+
size_t warnoption_len = wcslen(static_warnoption);
176+
size_t xoption_len = wcslen(static_xoption);
177+
wchar_t *dynamic_once_warnoption = calloc(warnoption_len+1, sizeof(wchar_t));
178+
wchar_t *dynamic_xoption = calloc(xoption_len+1, sizeof(wchar_t));
179+
wcsncpy(dynamic_once_warnoption, static_warnoption, warnoption_len+1);
180+
wcsncpy(dynamic_xoption, static_xoption, xoption_len+1);
181+
182+
_Py_EMBED_PREINIT_CHECK("Checking PySys_AddWarnOption\n");
183+
PySys_AddWarnOption(L"default");
184+
_Py_EMBED_PREINIT_CHECK("Checking PySys_ResetWarnOptions\n");
185+
PySys_ResetWarnOptions();
186+
_Py_EMBED_PREINIT_CHECK("Checking PySys_AddWarnOption linked list\n");
187+
PySys_AddWarnOption(dynamic_once_warnoption);
188+
PySys_AddWarnOption(L"module");
189+
PySys_AddWarnOption(L"default");
190+
_Py_EMBED_PREINIT_CHECK("Checking PySys_AddXOption\n");
191+
PySys_AddXOption(L"not_an_option=1");
192+
PySys_AddXOption(dynamic_xoption);
193+
194+
/* Delete the dynamic options early */
195+
free(dynamic_once_warnoption);
196+
dynamic_once_warnoption = NULL;
197+
free(dynamic_xoption);
198+
dynamic_xoption = NULL;
199+
200+
_Py_EMBED_PREINIT_CHECK("Initializing interpreter\n");
201+
_testembed_Py_Initialize();
202+
_Py_EMBED_PREINIT_CHECK("Check sys module contents\n");
203+
PyRun_SimpleString("import sys; "
204+
"print('sys.warnoptions:', sys.warnoptions); "
205+
"print('sys._xoptions:', sys._xoptions); "
206+
"warnings = sys.modules['warnings']; "
207+
"latest_filters = [f[0] for f in warnings.filters[:3]]; "
208+
"print('warnings.filters[:3]:', latest_filters)");
209+
_Py_EMBED_PREINIT_CHECK("Finalizing interpreter\n");
210+
Py_Finalize();
211+
212+
return 0;
213+
}
214+
215+
216+
/* bpo-20891: Avoid race condition when initialising the GIL */
150217
static void bpo20891_thread(void *lockp)
151218
{
152219
PyThread_type_lock lock = *((PyThread_type_lock*)lockp);
@@ -217,6 +284,7 @@ static struct TestCase TestCases[] = {
217284
{ "forced_io_encoding", test_forced_io_encoding },
218285
{ "repeated_init_and_subinterpreters", test_repeated_init_and_subinterpreters },
219286
{ "pre_initialization_api", test_pre_initialization_api },
287+
{ "pre_initialization_sys_options", test_pre_initialization_sys_options },
220288
{ "bpo20891", test_bpo20891 },
221289
{ NULL, NULL }
222290
};
@@ -232,13 +300,13 @@ int main(int argc, char *argv[])
232300

233301
/* No match found, or no test name provided, so display usage */
234302
printf("Python " PY_VERSION " _testembed executable for embedded interpreter tests\n"
235-
"Normally executed via 'EmbeddingTests' in Lib/test/test_capi.py\n\n"
303+
"Normally executed via 'EmbeddingTests' in Lib/test/test_embed.py\n\n"
236304
"Usage: %s TESTNAME\n\nAll available tests:\n", argv[0]);
237305
for (struct TestCase *tc = TestCases; tc && tc->name; tc++) {
238306
printf(" %s\n", tc->name);
239307
}
240308

241-
/* Non-zero exit code will cause test_capi.py tests to fail.
309+
/* Non-zero exit code will cause test_embed.py tests to fail.
242310
This is intentional. */
243311
return -1;
244312
}

0 commit comments

Comments
 (0)