Skip to content

Commit fb4ae15

Browse files
authored
bpo-38317: Fix PyConfig.warnoptions priority (GH-16478)
Fix warnings options priority: PyConfig.warnoptions has the highest priority, as stated in the PEP 587. * Document options order in PyConfig.warnoptions documentation. * Make PyWideStringList_INIT macro private: replace "Py" prefix with "_Py". * test_embed: add test_init_warnoptions().
1 parent 58498bc commit fb4ae15

File tree

10 files changed

+185
-51
lines changed

10 files changed

+185
-51
lines changed

Doc/c-api/init_config.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,13 @@ PyConfig
704704
705705
.. c:member:: PyWideStringList warnoptions
706706
707-
Options of the :mod:`warnings` module to build warnings filters.
707+
:data:`sys.warnoptions`: options of the :mod:`warnings` module to build
708+
warnings filters: lowest to highest priority.
709+
710+
The :mod:`warnings` module adds :data:`sys.warnoptions` in the reverse
711+
order: the last :c:member:`PyConfig.warnoptions` item becomes the first
712+
item of :data:`warnings.filters` which is checked first (highest
713+
priority).
708714
709715
.. c:member:: int write_bytecode
710716

Include/cpython/initconfig.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ typedef struct {
220220
wchar_t *program_name;
221221

222222
PyWideStringList xoptions; /* Command line -X options */
223-
PyWideStringList warnoptions; /* Warnings options */
223+
224+
/* Warnings options: lowest to highest priority. warnings.filters
225+
is built in the reverse order (highest to lowest priority). */
226+
PyWideStringList warnoptions;
224227

225228
/* If equal to zero, disable the import of the module site and the
226229
site-dependent manipulations of sys.path that it entails. Also disable

Include/internal/pycore_initconfig.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extern "C" {
4545

4646
/* --- PyWideStringList ------------------------------------------------ */
4747

48-
#define PyWideStringList_INIT (PyWideStringList){.length = 0, .items = NULL}
48+
#define _PyWideStringList_INIT (PyWideStringList){.length = 0, .items = NULL}
4949

5050
#ifndef NDEBUG
5151
PyAPI_FUNC(int) _PyWideStringList_CheckConsistency(const PyWideStringList *list);

Include/internal/pycore_pylifecycle.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ extern PyStatus _PySys_Create(
4141
PyThreadState *tstate,
4242
PyObject **sysmod_p);
4343
extern PyStatus _PySys_SetPreliminaryStderr(PyObject *sysdict);
44-
extern PyStatus _PySys_ReadPreinitWarnOptions(PyConfig *config);
44+
extern PyStatus _PySys_ReadPreinitWarnOptions(PyWideStringList *options);
4545
extern PyStatus _PySys_ReadPreinitXOptions(PyConfig *config);
4646
extern int _PySys_InitMain(
4747
_PyRuntimeState *runtime,

Lib/test/test_embed.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,9 @@ def test_init_from_config(self):
746746
'cmdline_xoption',
747747
],
748748
'warnoptions': [
749-
'config_warnoption',
750749
'cmdline_warnoption',
751750
'default::BytesWarning',
751+
'config_warnoption',
752752
],
753753
'run_command': 'pass\n',
754754

@@ -952,9 +952,9 @@ def test_init_sys_add(self):
952952
'faulthandler',
953953
],
954954
'warnoptions': [
955-
'ignore:::config_warnoption',
956955
'ignore:::cmdline_warnoption',
957956
'ignore:::sysadd_warnoption',
957+
'ignore:::config_warnoption',
958958
],
959959
}
960960
self.check_all_configs("test_init_sys_add", config, api=API_PYTHON)
@@ -1268,6 +1268,30 @@ def get_func(name):
12681268
self.assertEqual(Py_GetProgramFullPath(), config['executable'])
12691269
self.assertEqual(Py_GetPythonHome(), config['home'])
12701270

1271+
def test_init_warnoptions(self):
1272+
# lowest to highest priority
1273+
warnoptions = [
1274+
'ignore:::PyConfig_Insert0', # PyWideStringList_Insert(0)
1275+
'default', # PyConfig.dev_mode=1
1276+
'ignore:::env1', # PYTHONWARNINGS env var
1277+
'ignore:::env2', # PYTHONWARNINGS env var
1278+
'ignore:::cmdline1', # -W opt command line option
1279+
'ignore:::cmdline2', # -W opt command line option
1280+
'default::BytesWarning', # PyConfig.bytes_warnings=1
1281+
'ignore:::PySys_AddWarnOption1', # PySys_AddWarnOption()
1282+
'ignore:::PySys_AddWarnOption2', # PySys_AddWarnOption()
1283+
'ignore:::PyConfig_BeforeRead', # PyConfig.warnoptions
1284+
'ignore:::PyConfig_AfterRead'] # PyWideStringList_Append()
1285+
preconfig = dict(allocator=PYMEM_ALLOCATOR_DEBUG)
1286+
config = {
1287+
'dev_mode': 1,
1288+
'faulthandler': 1,
1289+
'bytes_warning': 1,
1290+
'warnoptions': warnoptions,
1291+
}
1292+
self.check_all_configs("test_init_warnoptions", config, preconfig,
1293+
api=API_PYTHON)
1294+
12711295

12721296
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
12731297
def test_open_code_hook(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix warnings options priority: ``PyConfig.warnoptions`` has the highest
2+
priority, as stated in the :pep:`587`.

Programs/_testembed.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,64 @@ static int test_init_setpythonhome(void)
16031603
}
16041604

16051605

1606+
static int test_init_warnoptions(void)
1607+
{
1608+
PyStatus status;
1609+
putenv("PYTHONWARNINGS=ignore:::env1,ignore:::env2");
1610+
1611+
PySys_AddWarnOption(L"ignore:::PySys_AddWarnOption1");
1612+
PySys_AddWarnOption(L"ignore:::PySys_AddWarnOption2");
1613+
1614+
PyConfig config;
1615+
config.struct_size = sizeof(PyConfig);
1616+
1617+
status = PyConfig_InitPythonConfig(&config);
1618+
if (PyStatus_Exception(status)) {
1619+
Py_ExitStatusException(status);
1620+
}
1621+
1622+
config.dev_mode = 1;
1623+
config.bytes_warning = 1;
1624+
1625+
config_set_program_name(&config);
1626+
1627+
status = PyWideStringList_Append(&config.warnoptions,
1628+
L"ignore:::PyConfig_BeforeRead");
1629+
if (PyStatus_Exception(status)) {
1630+
Py_ExitStatusException(status);
1631+
}
1632+
1633+
wchar_t* argv[] = {
1634+
L"python3",
1635+
L"-Wignore:::cmdline1",
1636+
L"-Wignore:::cmdline2"};
1637+
config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv);
1638+
config.parse_argv = 1;
1639+
1640+
status = PyConfig_Read(&config);
1641+
if (PyStatus_Exception(status)) {
1642+
Py_ExitStatusException(status);
1643+
}
1644+
1645+
status = PyWideStringList_Append(&config.warnoptions,
1646+
L"ignore:::PyConfig_AfterRead");
1647+
if (PyStatus_Exception(status)) {
1648+
Py_ExitStatusException(status);
1649+
}
1650+
1651+
status = PyWideStringList_Insert(&config.warnoptions,
1652+
0, L"ignore:::PyConfig_Insert0");
1653+
if (PyStatus_Exception(status)) {
1654+
Py_ExitStatusException(status);
1655+
}
1656+
1657+
init_from_config_clear(&config);
1658+
dump_config();
1659+
Py_Finalize();
1660+
return 0;
1661+
}
1662+
1663+
16061664
static void configure_init_main(PyConfig *config)
16071665
{
16081666
wchar_t* argv[] = {
@@ -1746,6 +1804,7 @@ static struct TestCase TestCases[] = {
17461804
{"test_init_setpath", test_init_setpath},
17471805
{"test_init_setpath_config", test_init_setpath_config},
17481806
{"test_init_setpythonhome", test_init_setpythonhome},
1807+
{"test_init_warnoptions", test_init_warnoptions},
17491808
{"test_run_main", test_run_main},
17501809

17511810
{"test_open_code_hook", test_open_code_hook},

Python/initconfig.c

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ _PyWideStringList_Copy(PyWideStringList *list, const PyWideStringList *list2)
273273
return 0;
274274
}
275275

276-
PyWideStringList copy = PyWideStringList_INIT;
276+
PyWideStringList copy = _PyWideStringList_INIT;
277277

278278
size_t size = list2->length * sizeof(list2->items[0]);
279279
copy.items = PyMem_RawMalloc(size);
@@ -2095,63 +2095,83 @@ config_init_env_warnoptions(PyConfig *config, PyWideStringList *warnoptions)
20952095

20962096

20972097
static PyStatus
2098-
config_add_warnoption(PyConfig *config, const wchar_t *option)
2098+
warnoptions_append(PyConfig *config, PyWideStringList *options,
2099+
const wchar_t *option)
20992100
{
2101+
/* config_init_warnoptions() add existing config warnoptions at the end:
2102+
ensure that the new option is not already present in this list to
2103+
prevent change the options order whne config_init_warnoptions() is
2104+
called twice. */
21002105
if (_PyWideStringList_Find(&config->warnoptions, option)) {
21012106
/* Already present: do nothing */
21022107
return _PyStatus_OK();
21032108
}
2104-
return PyWideStringList_Append(&config->warnoptions, option);
2109+
if (_PyWideStringList_Find(options, option)) {
2110+
/* Already present: do nothing */
2111+
return _PyStatus_OK();
2112+
}
2113+
return PyWideStringList_Append(options, option);
2114+
}
2115+
2116+
2117+
static PyStatus
2118+
warnoptions_extend(PyConfig *config, PyWideStringList *options,
2119+
const PyWideStringList *options2)
2120+
{
2121+
const Py_ssize_t len = options2->length;
2122+
wchar_t *const *items = options2->items;
2123+
2124+
for (Py_ssize_t i = 0; i < len; i++) {
2125+
PyStatus status = warnoptions_append(config, options, items[i]);
2126+
if (_PyStatus_EXCEPTION(status)) {
2127+
return status;
2128+
}
2129+
}
2130+
return _PyStatus_OK();
21052131
}
21062132

21072133

21082134
static PyStatus
21092135
config_init_warnoptions(PyConfig *config,
21102136
const PyWideStringList *cmdline_warnoptions,
2111-
const PyWideStringList *env_warnoptions)
2137+
const PyWideStringList *env_warnoptions,
2138+
const PyWideStringList *sys_warnoptions)
21122139
{
21132140
PyStatus status;
2141+
PyWideStringList options = _PyWideStringList_INIT;
21142142

2115-
/* The priority order for warnings configuration is (highest precedence
2116-
* first):
2143+
/* Priority of warnings options, lowest to highest:
21172144
*
2118-
* - early PySys_AddWarnOption() calls
2119-
* - the BytesWarning filter, if needed ('-b', '-bb')
2120-
* - any '-W' command line options; then
2121-
* - the 'PYTHONWARNINGS' environment variable; then
2122-
* - the dev mode filter ('-X dev', 'PYTHONDEVMODE'); then
21232145
* - any implicit filters added by _warnings.c/warnings.py
2146+
* - PyConfig.dev_mode: "default" filter
2147+
* - PYTHONWARNINGS environment variable
2148+
* - '-W' command line options
2149+
* - PyConfig.bytes_warning ('-b' and '-bb' command line options):
2150+
* "default::BytesWarning" or "error::BytesWarning" filter
2151+
* - early PySys_AddWarnOption() calls
2152+
* - PyConfig.warnoptions
21242153
*
2125-
* All settings except the last are passed to the warnings module via
2126-
* the `sys.warnoptions` list. Since the warnings module works on the basis
2127-
* of "the most recently added filter will be checked first", we add
2128-
* the lowest precedence entries first so that later entries override them.
2154+
* PyConfig.warnoptions is copied to sys.warnoptions. Since the warnings
2155+
* module works on the basis of "the most recently added filter will be
2156+
* checked first", we add the lowest precedence entries first so that later
2157+
* entries override them.
21292158
*/
21302159

21312160
if (config->dev_mode) {
2132-
status = config_add_warnoption(config, L"default");
2161+
status = warnoptions_append(config, &options, L"default");
21332162
if (_PyStatus_EXCEPTION(status)) {
2134-
return status;
2163+
goto error;
21352164
}
21362165
}
21372166

2138-
Py_ssize_t i;
2139-
const PyWideStringList *options;
2140-
2141-
options = env_warnoptions;
2142-
for (i = 0; i < options->length; i++) {
2143-
status = config_add_warnoption(config, options->items[i]);
2144-
if (_PyStatus_EXCEPTION(status)) {
2145-
return status;
2146-
}
2167+
status = warnoptions_extend(config, &options, env_warnoptions);
2168+
if (_PyStatus_EXCEPTION(status)) {
2169+
goto error;
21472170
}
21482171

2149-
options = cmdline_warnoptions;
2150-
for (i = 0; i < options->length; i++) {
2151-
status = config_add_warnoption(config, options->items[i]);
2152-
if (_PyStatus_EXCEPTION(status)) {
2153-
return status;
2154-
}
2172+
status = warnoptions_extend(config, &options, cmdline_warnoptions);
2173+
if (_PyStatus_EXCEPTION(status)) {
2174+
goto error;
21552175
}
21562176

21572177
/* If the bytes_warning_flag isn't set, bytesobject.c and bytearrayobject.c
@@ -2166,27 +2186,38 @@ config_init_warnoptions(PyConfig *config,
21662186
else {
21672187
filter = L"default::BytesWarning";
21682188
}
2169-
status = config_add_warnoption(config, filter);
2189+
status = warnoptions_append(config, &options, filter);
21702190
if (_PyStatus_EXCEPTION(status)) {
2171-
return status;
2191+
goto error;
21722192
}
21732193
}
21742194

2175-
/* Handle early PySys_AddWarnOption() calls */
2176-
status = _PySys_ReadPreinitWarnOptions(config);
2195+
status = warnoptions_extend(config, &options, sys_warnoptions);
21772196
if (_PyStatus_EXCEPTION(status)) {
2178-
return status;
2197+
goto error;
2198+
}
2199+
2200+
/* Always add all PyConfig.warnoptions options */
2201+
status = _PyWideStringList_Extend(&options, &config->warnoptions);
2202+
if (_PyStatus_EXCEPTION(status)) {
2203+
goto error;
21792204
}
21802205

2206+
_PyWideStringList_Clear(&config->warnoptions);
2207+
config->warnoptions = options;
21812208
return _PyStatus_OK();
2209+
2210+
error:
2211+
_PyWideStringList_Clear(&options);
2212+
return status;
21822213
}
21832214

21842215

21852216
static PyStatus
21862217
config_update_argv(PyConfig *config, Py_ssize_t opt_index)
21872218
{
21882219
const PyWideStringList *cmdline_argv = &config->argv;
2189-
PyWideStringList config_argv = PyWideStringList_INIT;
2220+
PyWideStringList config_argv = _PyWideStringList_INIT;
21902221

21912222
/* Copy argv to be able to modify it (to force -c/-m) */
21922223
if (cmdline_argv->length <= opt_index) {
@@ -2306,8 +2337,9 @@ static PyStatus
23062337
config_read_cmdline(PyConfig *config)
23072338
{
23082339
PyStatus status;
2309-
PyWideStringList cmdline_warnoptions = PyWideStringList_INIT;
2310-
PyWideStringList env_warnoptions = PyWideStringList_INIT;
2340+
PyWideStringList cmdline_warnoptions = _PyWideStringList_INIT;
2341+
PyWideStringList env_warnoptions = _PyWideStringList_INIT;
2342+
PyWideStringList sys_warnoptions = _PyWideStringList_INIT;
23112343

23122344
if (config->parse_argv < 0) {
23132345
config->parse_argv = 1;
@@ -2351,9 +2383,16 @@ config_read_cmdline(PyConfig *config)
23512383
}
23522384
}
23532385

2386+
/* Handle early PySys_AddWarnOption() calls */
2387+
status = _PySys_ReadPreinitWarnOptions(&sys_warnoptions);
2388+
if (_PyStatus_EXCEPTION(status)) {
2389+
goto done;
2390+
}
2391+
23542392
status = config_init_warnoptions(config,
23552393
&cmdline_warnoptions,
2356-
&env_warnoptions);
2394+
&env_warnoptions,
2395+
&sys_warnoptions);
23572396
if (_PyStatus_EXCEPTION(status)) {
23582397
goto done;
23592398
}
@@ -2363,6 +2402,7 @@ config_read_cmdline(PyConfig *config)
23632402
done:
23642403
_PyWideStringList_Clear(&cmdline_warnoptions);
23652404
_PyWideStringList_Clear(&env_warnoptions);
2405+
_PyWideStringList_Clear(&sys_warnoptions);
23662406
return status;
23672407
}
23682408

@@ -2433,7 +2473,7 @@ PyStatus
24332473
PyConfig_Read(PyConfig *config)
24342474
{
24352475
PyStatus status;
2436-
PyWideStringList orig_argv = PyWideStringList_INIT;
2476+
PyWideStringList orig_argv = _PyWideStringList_INIT;
24372477

24382478
status = config_check_struct_size(config);
24392479
if (_PyStatus_EXCEPTION(status)) {

Python/preconfig.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ _Py_SetFileSystemEncoding(const char *encoding, const char *errors)
7575
PyStatus
7676
_PyArgv_AsWstrList(const _PyArgv *args, PyWideStringList *list)
7777
{
78-
PyWideStringList wargv = PyWideStringList_INIT;
78+
PyWideStringList wargv = _PyWideStringList_INIT;
7979
if (args->use_bytes_argv) {
8080
size_t size = sizeof(wchar_t*) * args->argc;
8181
wargv.items = (wchar_t **)PyMem_RawMalloc(size);

0 commit comments

Comments
 (0)