Skip to content

Commit f37f14c

Browse files
gh-135801: Improve filtering by module in warn_explicit() without module argument
* Try to match the module name pattern with module names constructed starting from different parent directories of the filename. E.g., for "/path/to/package/module" try to match with "path.to.package.module", "to.package.module", "package.module" and "module". * Ignore trailing "/__init__.py". * Ignore trailing ".py" on Windows. * Keep matching with the full filename (without optional ".py" extension) for compatibility. * Only ignore the case of the ".py" extension on Windows.
1 parent 7ac94fc commit f37f14c

File tree

13 files changed

+340
-63
lines changed

13 files changed

+340
-63
lines changed

Doc/library/warnings.rst

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,27 @@ Available Functions
480480
.. function:: warn_explicit(message, category, filename, lineno, module=None, registry=None, module_globals=None, source=None)
481481

482482
This is a low-level interface to the functionality of :func:`warn`, passing in
483-
explicitly the message, category, filename and line number, and optionally the
484-
module name and the registry (which should be the ``__warningregistry__``
485-
dictionary of the module). The module name defaults to the filename with
486-
``.py`` stripped; if no registry is passed, the warning is never suppressed.
483+
explicitly the message, category, filename and line number, and optionally
484+
other arguments.
487485
*message* must be a string and *category* a subclass of :exc:`Warning` or
488486
*message* may be a :exc:`Warning` instance, in which case *category* will be
489487
ignored.
490488

489+
*module*, if supplied, should be the module name.
490+
If no module is passed, the module regular expression in
491+
:ref:`warnings filter <warning-filter>` will be tested against the filename
492+
with ``/__init__.py`` and ``.py`` (and ``.pyw`` on Windows) stripped and
493+
against the module names constructed from the path components starting
494+
from all parent directories.
495+
For example, when filename is ``'/path/to/package/module.py'``, it will
496+
be tested against ``'/path/to/package/module'``,
497+
``'path.to.package.module'``, ``'to.package.module'``
498+
``'package.module'`` and ``'module'``.
499+
500+
*registry*, if supplied, should be the ``__warningregistry__`` dictionary
501+
of the module.
502+
If no registry is passed, each warning is treated as the first occurrence.
503+
491504
*module_globals*, if supplied, should be the global namespace in use by the code
492505
for which the warning is issued. (This argument is used to support displaying
493506
source for modules found in zipfiles or other non-filesystem import
@@ -499,6 +512,11 @@ Available Functions
499512
.. versionchanged:: 3.6
500513
Add the *source* parameter.
501514

515+
.. versionchanged:: next
516+
If no module is passed, test the filter regular expression against
517+
module names created from the path, not only the path itself;
518+
strip also ``.pyw`` (on Windows) and ``/__init__.py``.
519+
502520

503521
.. function:: showwarning(message, category, filename, lineno, file=None, line=None)
504522

Doc/whatsnew/3.15.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,18 @@ unittest
601601
(Contributed by Garry Cairns in :gh:`134567`.)
602602

603603

604+
warnings
605+
--------
606+
607+
* Improve filtering by module in :func:`warnings.warn_explicit` if no *module*
608+
argument is passed.
609+
It now tests the module regular expression in the warnings filter not only
610+
against the filename with ``.py`` stripped, but also against module names
611+
constructed starting from different parent directories of the filename.
612+
Strip also ``.pyw`` (on Windows) and ``/__init__.py``.
613+
(Contributed by Serhiy Storchaka in :gh:`135801`.)
614+
615+
604616
xml.parsers.expat
605617
-----------------
606618

Lib/_py_warnings.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,20 +520,54 @@ def warn(message, category=None, stacklevel=1, source=None,
520520
)
521521

522522

523+
def _match_filename(pattern, filename, *, MS_WINDOWS=(sys.platform == 'win32')):
524+
if not filename:
525+
return pattern.match('<unknown>') is not None
526+
if filename[0] == '<' and filename[-1] == '>':
527+
return pattern.match(filename) is not None
528+
529+
if MS_WINDOWS:
530+
if filename[-12:].lower() in (r'\__init__.py', '/__init__.py'):
531+
if pattern.match(filename[:-3]): # without '.py'
532+
return True
533+
filename = filename[:-12]
534+
elif filename[-3:].lower() == '.py':
535+
filename = filename[:-3]
536+
elif filename[-4:].lower() == '.pyw':
537+
filename = filename[:-4]
538+
if pattern.match(filename):
539+
return True
540+
filename = filename.replace('\\', '/')
541+
else:
542+
if filename.endswith('/__init__.py'):
543+
if pattern.match(filename[:-3]): # without '.py'
544+
return True
545+
filename = filename[:-12]
546+
elif filename.endswith('.py'):
547+
filename = filename[:-3]
548+
if pattern.match(filename):
549+
return True
550+
filename = filename.replace('/', '.')
551+
i = 0
552+
while True:
553+
if pattern.match(filename, i):
554+
return True
555+
i = filename.find('.', i) + 1
556+
if not i:
557+
return False
558+
559+
523560
def warn_explicit(message, category, filename, lineno,
524561
module=None, registry=None, module_globals=None,
525562
source=None):
526563
lineno = int(lineno)
527-
if module is None:
528-
module = filename or "<unknown>"
529-
if module[-3:].lower() == ".py":
530-
module = module[:-3] # XXX What about leading pathname?
531564
if isinstance(message, Warning):
532565
text = str(message)
533566
category = message.__class__
534567
else:
535568
text = message
536569
message = category(message)
570+
modules = None
537571
key = (text, category, lineno)
538572
with _wm._lock:
539573
if registry is None:
@@ -549,9 +583,11 @@ def warn_explicit(message, category, filename, lineno,
549583
action, msg, cat, mod, ln = item
550584
if ((msg is None or msg.match(text)) and
551585
issubclass(category, cat) and
552-
(mod is None or mod.match(module)) and
553-
(ln == 0 or lineno == ln)):
554-
break
586+
(ln == 0 or lineno == ln) and
587+
(mod is None or (_match_filename(mod, filename)
588+
if module is None else
589+
mod.match(module)))):
590+
break
555591
else:
556592
action = _wm.defaultaction
557593
# Early exit actions

Lib/test/test_ast/test_ast.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import textwrap
1414
import types
1515
import unittest
16+
import warnings
1617
import weakref
1718
from io import StringIO
1819
from pathlib import Path
@@ -1124,6 +1125,19 @@ def test_tstring(self):
11241125
self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
11251126
self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
11261127

1128+
def test_filter_syntax_warnings_by_module(self):
1129+
filename = support.findfile('test_import/data/syntax_warnings.py')
1130+
with open(filename, 'rb') as f:
1131+
source = f.read()
1132+
with warnings.catch_warnings(record=True) as wlog:
1133+
warnings.simplefilter('error')
1134+
warnings.filterwarnings('always', module=r'<unknown>\z')
1135+
ast.parse(source)
1136+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 21])
1137+
for wm in wlog:
1138+
self.assertEqual(wm.filename, '<unknown>')
1139+
self.assertIs(wm.category, SyntaxWarning)
1140+
11271141

11281142
class CopyTests(unittest.TestCase):
11291143
"""Test copying and pickling AST nodes."""

Lib/test/test_builtin.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,28 @@ def four_freevars():
10881088
three_freevars.__globals__,
10891089
closure=my_closure)
10901090

1091+
def test_exec_filter_syntax_warnings_by_module(self):
1092+
filename = support.findfile('test_import/data/syntax_warnings.py')
1093+
with open(filename, 'rb') as f:
1094+
source = f.read()
1095+
with warnings.catch_warnings(record=True) as wlog:
1096+
warnings.simplefilter('error')
1097+
warnings.filterwarnings('always', module=r'<string>\z')
1098+
exec(source, {})
1099+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
1100+
for wm in wlog:
1101+
self.assertEqual(wm.filename, '<string>')
1102+
self.assertIs(wm.category, SyntaxWarning)
1103+
1104+
with warnings.catch_warnings(record=True) as wlog:
1105+
warnings.simplefilter('error')
1106+
warnings.filterwarnings('always', module=r'<string>\z')
1107+
exec(source, {'__name__': 'package.module', '__file__': filename})
1108+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
1109+
for wm in wlog:
1110+
self.assertEqual(wm.filename, '<string>')
1111+
self.assertIs(wm.category, SyntaxWarning)
1112+
10911113

10921114
def test_filter(self):
10931115
self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld'))

Lib/test/test_cmd_line_script.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,12 @@ def test_script_as_dev_fd(self):
810810
out, err = p.communicate()
811811
self.assertEqual(out, b"12345678912345678912345\n")
812812

813+
def test_filter_syntax_warnings_by_module(self):
814+
filename = support.findfile('test_import/data/syntax_warnings.py')
815+
rc, out, err = assert_python_ok('-Werror', '-Walways:::test.test_import.data.syntax_warnings', filename)
816+
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
817+
rc, out, err = assert_python_ok('-Werror', '-Walways:::syntax_warnings', filename)
818+
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
813819

814820

815821
def tearDownModule():

Lib/test/test_compile.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,20 @@ def test_compile_warning_in_finally(self):
17451745
self.assertEqual(wm.category, SyntaxWarning)
17461746
self.assertIn("\"is\" with 'int' literal", str(wm.message))
17471747

1748+
def test_filter_syntax_warnings_by_module(self):
1749+
filename = support.findfile('test_import/data/syntax_warnings.py')
1750+
with open(filename, 'rb') as f:
1751+
source = f.read()
1752+
module_re = r'test\.test_import\.data\.syntax_warnings\z'
1753+
with warnings.catch_warnings(record=True) as wlog:
1754+
warnings.simplefilter('error')
1755+
warnings.filterwarnings('always', module=module_re)
1756+
compile(source, filename, 'exec')
1757+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
1758+
for wm in wlog:
1759+
self.assertEqual(wm.filename, filename)
1760+
self.assertIs(wm.category, SyntaxWarning)
1761+
17481762

17491763
class TestBooleanExpression(unittest.TestCase):
17501764
class Value:

Lib/test/test_import/__init__.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import os
1616
import py_compile
1717
import random
18+
import re
1819
import shutil
1920
import stat
2021
import subprocess
@@ -23,6 +24,7 @@
2324
import threading
2425
import time
2526
import types
27+
import warnings
2628
import unittest
2729
from unittest import mock
2830
import _imp
@@ -51,7 +53,7 @@
5153
TESTFN, rmtree, temp_umask, TESTFN_UNENCODABLE)
5254
from test.support import script_helper
5355
from test.support import threading_helper
54-
from test.test_importlib.util import uncache
56+
from test.test_importlib.util import uncache, temporary_pycache_prefix
5557
from types import ModuleType
5658
try:
5759
import _testsinglephase
@@ -412,7 +414,6 @@ def test_from_import_missing_attr_path_is_canonical(self):
412414
self.assertIsNotNone(cm.exception)
413415

414416
def test_from_import_star_invalid_type(self):
415-
import re
416417
with ready_to_import() as (name, path):
417418
with open(path, 'w', encoding='utf-8') as f:
418419
f.write("__all__ = [b'invalid_type']")
@@ -1250,6 +1251,35 @@ class Spec2:
12501251
origin = "a\x00b"
12511252
_imp.create_dynamic(Spec2())
12521253

1254+
def test_filter_syntax_warnings_by_module(self):
1255+
module_re = r'test\.test_import\.data\.syntax_warnings\z'
1256+
unload('test.test_import.data.syntax_warnings')
1257+
with (os_helper.temp_dir() as tmpdir,
1258+
temporary_pycache_prefix(tmpdir),
1259+
warnings.catch_warnings(record=True) as wlog):
1260+
warnings.simplefilter('error')
1261+
warnings.filterwarnings('always', module=module_re)
1262+
import test.test_import.data.syntax_warnings
1263+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
1264+
filename = test.test_import.data.syntax_warnings.__file__
1265+
for wm in wlog:
1266+
self.assertEqual(wm.filename, filename)
1267+
self.assertIs(wm.category, SyntaxWarning)
1268+
1269+
module_re = r'syntax_warnings\z'
1270+
unload('test.test_import.data.syntax_warnings')
1271+
with (os_helper.temp_dir() as tmpdir,
1272+
temporary_pycache_prefix(tmpdir),
1273+
warnings.catch_warnings(record=True) as wlog):
1274+
warnings.simplefilter('error')
1275+
warnings.filterwarnings('always', module=module_re)
1276+
import test.test_import.data.syntax_warnings
1277+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
1278+
filename = test.test_import.data.syntax_warnings.__file__
1279+
for wm in wlog:
1280+
self.assertEqual(wm.filename, filename)
1281+
self.assertIs(wm.category, SyntaxWarning)
1282+
12531283

12541284
@skip_if_dont_write_bytecode
12551285
class FilePermissionTests(unittest.TestCase):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Syntax warnings emitted in different parts of the Python compiler.
2+
3+
# Parser/lexer/lexer.c
4+
x = 1or 0 # line 4
5+
6+
# Parser/tokenizer/helpers.c
7+
'\z' # line 7
8+
9+
# Parser/string_parser.c
10+
'\400' # line 10
11+
12+
# _PyCompile_Warn() in Python/codegen.c
13+
assert(x, 'message') # line 13
14+
x is 1 # line 14
15+
16+
# _PyErr_EmitSyntaxWarning() in Python/ast_preprocess.c
17+
def f():
18+
try:
19+
pass
20+
finally:
21+
return 42 # line 21

Lib/test/test_symtable.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import textwrap
77
import symtable
8+
import warnings
89
import unittest
910

1011
from test import support
@@ -586,6 +587,20 @@ def test__symtable_refleak(self):
586587
# check error path when 'compile_type' AC conversion failed
587588
self.assertRaises(TypeError, symtable.symtable, '', mortal_str, 1)
588589

590+
def test_filter_syntax_warnings_by_module(self):
591+
filename = support.findfile('test_import/data/syntax_warnings.py')
592+
with open(filename, 'rb') as f:
593+
source = f.read()
594+
module_re = r'test\.test_import\.data\.syntax_warnings\z'
595+
with warnings.catch_warnings(record=True) as wlog:
596+
warnings.simplefilter('error')
597+
warnings.filterwarnings('always', module=module_re)
598+
symtable.symtable(source, filename, 'exec')
599+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
600+
for wm in wlog:
601+
self.assertEqual(wm.filename, filename)
602+
self.assertIs(wm.category, SyntaxWarning)
603+
589604

590605
class ComprehensionTests(unittest.TestCase):
591606
def get_identifiers_recursive(self, st, res):

0 commit comments

Comments
 (0)