Skip to content

Commit 747f48e

Browse files
authored
bpo-32230: Set sys.warnoptions with -X dev (#4820)
Rather than supporting dev mode directly in the warnings module, this instead adjusts the initialisation code to add an extra 'default' entry to sys.warnoptions when dev mode is enabled. This ensures that dev mode behaves *exactly* as if `-Wdefault` had been passed on the command line, including in the way it interacts with `sys.warnoptions`, and with other command line flags like `-bb`. Fix also bpo-20361: have -b & -bb options take precedence over any other warnings options. Patch written by Nick Coghlan, with minor modifications of Victor Stinner.
1 parent b748e3b commit 747f48e

File tree

12 files changed

+224
-124
lines changed

12 files changed

+224
-124
lines changed

Doc/tools/susp-ignored.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,6 @@ whatsnew/changelog,,:end,str[start:end]
327327
library/binascii,,`,'`'
328328
library/uu,,`,'`'
329329
whatsnew/3.7,,`,'`'
330+
whatsnew/3.7,,::,error::BytesWarning
331+
whatsnew/changelog,,::,error::BytesWarning
332+
whatsnew/changelog,,::,default::BytesWarning

Doc/using/cmdline.rst

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,7 @@ Miscellaneous options
430430
not be more verbose than the default if the code is correct: new warnings
431431
are only emitted when an issue is detected. Effect of the developer mode:
432432

433-
* Warning filters: add a filter to display all warnings (``"default"``
434-
action), except of :exc:`BytesWarning` which still depends on the
435-
:option:`-b` option, and use ``"always"`` action for
436-
:exc:`ResourceWarning` warnings. For example, display
437-
:exc:`DeprecationWarning` warnings.
433+
* Add ``default`` warning filter, as :option:`-W` ``default``.
438434
* Install debug hooks on memory allocators: see the
439435
:c:func:`PyMem_SetupDebugHooks` C function.
440436
* Enable the :mod:`faulthandler` module to dump the Python traceback

Doc/whatsnew/3.7.rst

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,11 @@ resolution on Linux and Windows.
188188
New Development Mode: -X dev
189189
----------------------------
190190

191-
Add a new "development mode": ``-X dev`` command line option to enable debug
192-
checks at runtime.
193-
194-
In short, ``python3 -X dev ...`` behaves as ``PYTHONMALLOC=debug python3 -W
195-
default -X faulthandler ...``, except that the PYTHONMALLOC environment
196-
variable is not set in practice.
197-
198-
See :option:`-X` ``dev`` for the details.
191+
Add a new "development mode": :option:`-X` ``dev`` command line option and
192+
:envvar:`PYTHONDEVMODE` environment variable to enable CPython's "development
193+
mode", introducing additional runtime checks which are too expensive to be
194+
enabled by default. See :option:`-X` ``dev`` documentation for the effects of
195+
the development mode.
199196

200197
Hash-based pycs
201198
---------------
@@ -481,6 +478,29 @@ Function :func:`~uu.encode` now accepts an optional *backtick*
481478
keyword argument. When it's true, zeros are represented by ``'`'``
482479
instead of spaces. (Contributed by Xiang Zhang in :issue:`30103`.)
483480

481+
warnings
482+
--------
483+
484+
The initialization of the default warnings filters has changed as follows:
485+
486+
* warnings enabled via command line options (including those for :option:`-b`
487+
and the new CPython-specific ``-X dev`` option) are always passed to the
488+
warnings machinery via the ``sys.warnoptions`` attribute.
489+
* warnings filters enabled via the command line or the environment now have the
490+
following precedence order:
491+
492+
* the ``BytesWarning`` filter for :option:`-b` (or ``-bb``)
493+
* any filters specified with :option:`-W`
494+
* any filters specified with :envvar:`PYTHONWARNINGS`
495+
* any other CPython specific filters (e.g. the ``default`` filter added
496+
for the new ``-X dev`` mode)
497+
* any implicit filters defined directly by the warnings machinery
498+
* in CPython debug builds, all warnings are now displayed by default (the
499+
implicit filter list is empty)
500+
501+
(Contributed by Nick Coghlan and Victor Stinner in :issue:`20361`,
502+
:issue:`32043`, and :issue:`32230`)
503+
484504
xml.etree
485505
---------
486506

@@ -854,6 +874,12 @@ Other CPython implementation changes
854874
either in embedding applications, or in CPython itself.
855875
(Contributed by Nick Coghlan and Eric Snow as part of :issue:`22257`.)
856876

877+
* Due to changes in the way the default warnings filters are configured,
878+
setting ``Py_BytesWarningFlag`` to a value greater than one is no longer
879+
sufficient to both emit ``BytesWarning`` messages and have them converted
880+
to exceptions. Instead, the flag must be set (to cause the warnings to be
881+
emitted in the first place), and an explicit ``error::BytesWarning``
882+
warnings filter added to convert them to exceptions.
857883

858884
Documentation
859885
=============

Lib/subprocess.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def _optim_args_from_interpreter_flags():
241241

242242
def _args_from_interpreter_flags():
243243
"""Return a list of command-line arguments reproducing the current
244-
settings in sys.flags and sys.warnoptions."""
244+
settings in sys.flags, sys.warnoptions and sys._xoptions."""
245245
flag_opt_map = {
246246
'debug': 'd',
247247
# 'inspect': 'i',
@@ -262,12 +262,22 @@ def _args_from_interpreter_flags():
262262
args.append('-' + opt * v)
263263

264264
# -W options
265-
for opt in sys.warnoptions:
265+
warnopts = sys.warnoptions[:]
266+
bytes_warning = sys.flags.bytes_warning
267+
xoptions = getattr(sys, '_xoptions', {})
268+
dev_mode = ('dev' in xoptions)
269+
270+
if bytes_warning > 1:
271+
warnopts.remove("error::BytesWarning")
272+
elif bytes_warning:
273+
warnopts.remove("default::BytesWarning")
274+
if dev_mode:
275+
warnopts.remove('default')
276+
for opt in warnopts:
266277
args.append('-W' + opt)
267278

268279
# -X options
269-
xoptions = getattr(sys, '_xoptions', {})
270-
if 'dev' in xoptions:
280+
if dev_mode:
271281
args.extend(('-X', 'dev'))
272282
for opt in ('faulthandler', 'tracemalloc', 'importtime',
273283
'showalloccount', 'showrefcount'):

Lib/test/test_cmd_line.py

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
interpreter_requires_environment
1515
)
1616

17+
18+
# Debug build?
19+
Py_DEBUG = hasattr(sys, "gettotalrefcount")
20+
21+
1722
# XXX (ncoghlan): Move to script_helper and make consistent with run_python
1823
def _kill_python_and_exit_code(p):
1924
data = kill_python(p)
@@ -97,7 +102,7 @@ def run_python(*args):
97102
# "-X showrefcount" shows the refcount, but only in debug builds
98103
rc, out, err = run_python('-X', 'showrefcount', '-c', code)
99104
self.assertEqual(out.rstrip(), b"{'showrefcount': True}")
100-
if hasattr(sys, 'gettotalrefcount'): # debug build
105+
if Py_DEBUG:
101106
self.assertRegex(err, br'^\[\d+ refs, \d+ blocks\]')
102107
else:
103108
self.assertEqual(err, b'')
@@ -541,31 +546,26 @@ def test_xdev(self):
541546
code = ("import sys, warnings; "
542547
"print(' '.join('%s::%s' % (f[0], f[2].__name__) "
543548
"for f in warnings.filters))")
549+
if Py_DEBUG:
550+
expected_filters = "default::Warning"
551+
else:
552+
expected_filters = ("default::Warning "
553+
"ignore::DeprecationWarning "
554+
"ignore::PendingDeprecationWarning "
555+
"ignore::ImportWarning "
556+
"ignore::ResourceWarning")
544557

545558
out = self.run_xdev("-c", code)
546-
self.assertEqual(out,
547-
"ignore::BytesWarning "
548-
"default::ResourceWarning "
549-
"default::Warning")
559+
self.assertEqual(out, expected_filters)
550560

551561
out = self.run_xdev("-b", "-c", code)
552-
self.assertEqual(out,
553-
"default::BytesWarning "
554-
"default::ResourceWarning "
555-
"default::Warning")
562+
self.assertEqual(out, f"default::BytesWarning {expected_filters}")
556563

557564
out = self.run_xdev("-bb", "-c", code)
558-
self.assertEqual(out,
559-
"error::BytesWarning "
560-
"default::ResourceWarning "
561-
"default::Warning")
565+
self.assertEqual(out, f"error::BytesWarning {expected_filters}")
562566

563567
out = self.run_xdev("-Werror", "-c", code)
564-
self.assertEqual(out,
565-
"error::Warning "
566-
"ignore::BytesWarning "
567-
"default::ResourceWarning "
568-
"default::Warning")
568+
self.assertEqual(out, f"error::Warning {expected_filters}")
569569

570570
# Memory allocator debug hooks
571571
try:
@@ -592,6 +592,46 @@ def test_xdev(self):
592592
out = self.run_xdev("-c", code)
593593
self.assertEqual(out, "True")
594594

595+
def check_warnings_filters(self, cmdline_option, envvar, use_pywarning=False):
596+
if use_pywarning:
597+
code = ("import sys; from test.support import import_fresh_module; "
598+
"warnings = import_fresh_module('warnings', blocked=['_warnings']); ")
599+
else:
600+
code = "import sys, warnings; "
601+
code += ("print(' '.join('%s::%s' % (f[0], f[2].__name__) "
602+
"for f in warnings.filters))")
603+
args = (sys.executable, '-W', cmdline_option, '-bb', '-c', code)
604+
env = dict(os.environ)
605+
env.pop('PYTHONDEVMODE', None)
606+
env["PYTHONWARNINGS"] = envvar
607+
proc = subprocess.run(args,
608+
stdout=subprocess.PIPE,
609+
stderr=subprocess.STDOUT,
610+
universal_newlines=True,
611+
env=env)
612+
self.assertEqual(proc.returncode, 0, proc)
613+
return proc.stdout.rstrip()
614+
615+
def test_warnings_filter_precedence(self):
616+
expected_filters = ("error::BytesWarning "
617+
"once::UserWarning "
618+
"always::UserWarning")
619+
if not Py_DEBUG:
620+
expected_filters += (" "
621+
"ignore::DeprecationWarning "
622+
"ignore::PendingDeprecationWarning "
623+
"ignore::ImportWarning "
624+
"ignore::ResourceWarning")
625+
626+
out = self.check_warnings_filters("once::UserWarning",
627+
"always::UserWarning")
628+
self.assertEqual(out, expected_filters)
629+
630+
out = self.check_warnings_filters("once::UserWarning",
631+
"always::UserWarning",
632+
use_pywarning=True)
633+
self.assertEqual(out, expected_filters)
634+
595635
def check_pythonmalloc(self, env_var, name):
596636
code = 'import _testcapi; print(_testcapi.pymem_getallocatorsname())'
597637
env = dict(os.environ)
@@ -611,13 +651,12 @@ def check_pythonmalloc(self, env_var, name):
611651

612652
def test_pythonmalloc(self):
613653
# Test the PYTHONMALLOC environment variable
614-
pydebug = hasattr(sys, "gettotalrefcount")
615654
pymalloc = support.with_pymalloc()
616655
if pymalloc:
617-
default_name = 'pymalloc_debug' if pydebug else 'pymalloc'
656+
default_name = 'pymalloc_debug' if Py_DEBUG else 'pymalloc'
618657
default_name_debug = 'pymalloc_debug'
619658
else:
620-
default_name = 'malloc_debug' if pydebug else 'malloc'
659+
default_name = 'malloc_debug' if Py_DEBUG else 'malloc'
621660
default_name_debug = 'malloc_debug'
622661

623662
tests = [

Lib/test/test_warnings/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,28 +1110,32 @@ class EnvironmentVariableTests(BaseTest):
11101110
def test_single_warning(self):
11111111
rc, stdout, stderr = assert_python_ok("-c",
11121112
"import sys; sys.stdout.write(str(sys.warnoptions))",
1113-
PYTHONWARNINGS="ignore::DeprecationWarning")
1113+
PYTHONWARNINGS="ignore::DeprecationWarning",
1114+
PYTHONDEVMODE="")
11141115
self.assertEqual(stdout, b"['ignore::DeprecationWarning']")
11151116

11161117
def test_comma_separated_warnings(self):
11171118
rc, stdout, stderr = assert_python_ok("-c",
11181119
"import sys; sys.stdout.write(str(sys.warnoptions))",
1119-
PYTHONWARNINGS="ignore::DeprecationWarning,ignore::UnicodeWarning")
1120+
PYTHONWARNINGS="ignore::DeprecationWarning,ignore::UnicodeWarning",
1121+
PYTHONDEVMODE="")
11201122
self.assertEqual(stdout,
11211123
b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']")
11221124

11231125
def test_envvar_and_command_line(self):
11241126
rc, stdout, stderr = assert_python_ok("-Wignore::UnicodeWarning", "-c",
11251127
"import sys; sys.stdout.write(str(sys.warnoptions))",
1126-
PYTHONWARNINGS="ignore::DeprecationWarning")
1128+
PYTHONWARNINGS="ignore::DeprecationWarning",
1129+
PYTHONDEVMODE="")
11271130
self.assertEqual(stdout,
11281131
b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']")
11291132

11301133
def test_conflicting_envvar_and_command_line(self):
11311134
rc, stdout, stderr = assert_python_failure("-Werror::DeprecationWarning", "-c",
11321135
"import sys, warnings; sys.stdout.write(str(sys.warnoptions)); "
11331136
"warnings.warn('Message', DeprecationWarning)",
1134-
PYTHONWARNINGS="default::DeprecationWarning")
1137+
PYTHONWARNINGS="default::DeprecationWarning",
1138+
PYTHONDEVMODE="")
11351139
self.assertEqual(stdout,
11361140
b"['default::DeprecationWarning', 'error::DeprecationWarning']")
11371141
self.assertEqual(stderr.splitlines(),
@@ -1145,7 +1149,8 @@ def test_nonascii(self):
11451149
rc, stdout, stderr = assert_python_ok("-c",
11461150
"import sys; sys.stdout.write(str(sys.warnoptions))",
11471151
PYTHONIOENCODING="utf-8",
1148-
PYTHONWARNINGS="ignore:DeprecaciónWarning")
1152+
PYTHONWARNINGS="ignore:DeprecaciónWarning",
1153+
PYTHONDEVMODE="")
11491154
self.assertEqual(stdout,
11501155
"['ignore:DeprecaciónWarning']".encode('utf-8'))
11511156

Lib/unittest/test/test_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ def get_parse_out_err(p):
289289
at_msg = b'Please use assertTrue instead.'
290290

291291
# no args -> all the warnings are printed, unittest warnings only once
292-
p = subprocess.Popen([sys.executable, '_test_warnings.py'], **opts)
292+
p = subprocess.Popen([sys.executable, '-E', '_test_warnings.py'], **opts)
293293
with p:
294294
out, err = get_parse_out_err(p)
295295
self.assertIn(b'OK', err)

Lib/warnings.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -519,34 +519,11 @@ def _filters_mutated():
519519
# Module initialization
520520
_processoptions(sys.warnoptions)
521521
if not _warnings_defaults:
522-
dev_mode = ('dev' in getattr(sys, '_xoptions', {}))
523-
py_debug = hasattr(sys, 'gettotalrefcount')
524-
525-
if not(dev_mode or py_debug):
526-
silence = [ImportWarning, PendingDeprecationWarning]
527-
silence.append(DeprecationWarning)
528-
for cls in silence:
529-
simplefilter("ignore", category=cls)
530-
531-
bytes_warning = sys.flags.bytes_warning
532-
if bytes_warning > 1:
533-
bytes_action = "error"
534-
elif bytes_warning:
535-
bytes_action = "default"
536-
else:
537-
bytes_action = "ignore"
538-
simplefilter(bytes_action, category=BytesWarning, append=1)
539-
540-
# resource usage warnings are enabled by default in pydebug mode
541-
if dev_mode or py_debug:
542-
resource_action = "default"
543-
else:
544-
resource_action = "ignore"
545-
simplefilter(resource_action, category=ResourceWarning, append=1)
546-
547-
if dev_mode:
548-
simplefilter("default", category=Warning, append=1)
549-
550-
del py_debug, dev_mode
522+
# Several warning categories are ignored by default in Py_DEBUG builds
523+
if not hasattr(sys, 'gettotalrefcount'):
524+
simplefilter("ignore", category=DeprecationWarning, append=1)
525+
simplefilter("ignore", category=PendingDeprecationWarning, append=1)
526+
simplefilter("ignore", category=ImportWarning, append=1)
527+
simplefilter("ignore", category=ResourceWarning, append=1)
551528

552529
del _warnings_defaults
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
`-X dev` now injects a ``'default'`` entry into sys.warnoptions, ensuring
2+
that it behaves identically to actually passing ``-Wdefault`` at the command
3+
line.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``-b`` and ``-bb`` now inject ``'default::BytesWarning'`` and
2+
``error::BytesWarning`` entries into ``sys.warnoptions``, ensuring that they
3+
take precedence over any other warning filters configured via the ``-W``
4+
option or the ``PYTHONWARNINGS`` environment variable.

0 commit comments

Comments
 (0)