Skip to content

Commit a0aeb4f

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__". * Keep matching with the full filename (without optional ".py" extension) for compatibility. * Only ignore the case of the ".py" extension on Windows.
1 parent 4044255 commit a0aeb4f

File tree

10 files changed

+280
-58
lines changed

10 files changed

+280
-58
lines changed

Lib/_py_warnings.py

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

522522

523+
def _match_filename(pattern, filename):
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 sys.platform == 'win32':
530+
if filename[-3:].lower() == '.py':
531+
filename = filename[:-3]
532+
if pattern.match(filename):
533+
return True
534+
filename = filename.replace('\\', '/')
535+
else:
536+
filename = filename.removesuffix('.py')
537+
if pattern.match(filename):
538+
return True
539+
filename = filename.removesuffix('/__init__')
540+
filename = filename.replace('/', '.')
541+
i = 0
542+
while True:
543+
if pattern.match(filename, i):
544+
return True
545+
i = filename.find('.', i) + 1
546+
if not i:
547+
return False
548+
549+
523550
def warn_explicit(message, category, filename, lineno,
524551
module=None, registry=None, module_globals=None,
525552
source=None):
526553
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?
531554
if isinstance(message, Warning):
532555
text = str(message)
533556
category = message.__class__
534557
else:
535558
text = message
536559
message = category(message)
560+
modules = None
537561
key = (text, category, lineno)
538562
with _wm._lock:
539563
if registry is None:
@@ -549,9 +573,11 @@ def warn_explicit(message, category, filename, lineno,
549573
action, msg, cat, mod, ln = item
550574
if ((msg is None or msg.match(text)) and
551575
issubclass(category, cat) and
552-
(mod is None or mod.match(module)) and
553-
(ln == 0 or lineno == ln)):
554-
break
576+
(ln == 0 or lineno == ln) and
577+
(mod is None or (_match_filename(mod, filename)
578+
if module is None else
579+
mod.match(module)))):
580+
break
555581
else:
556582
action = _wm.defaultaction
557583
# 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_warnings/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_warnings/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_warnings/data/syntax_warnings.py')
815+
rc, out, err = assert_python_ok('-Werror', '-Walways:::test.test_warnings.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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import opcode
77
import os
88
import unittest
9+
import re
910
import sys
1011
import ast
1112
import _ast
@@ -1745,6 +1746,20 @@ def test_compile_warning_in_finally(self):
17451746
self.assertEqual(wm.category, SyntaxWarning)
17461747
self.assertIn("\"is\" with 'int' literal", str(wm.message))
17471748

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

17491764
class TestBooleanExpression(unittest.TestCase):
17501765
class Value:

Lib/test/test_import/__init__.py

Lines changed: 34 additions & 1 deletion
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
@@ -43,6 +45,7 @@
4345
Py_GIL_DISABLED,
4446
no_rerun,
4547
force_not_colorized_test_class,
48+
findfile
4649
)
4750
from test.support.import_helper import (
4851
forget, make_legacy_pyc, unlink, unload, ready_to_import,
@@ -51,7 +54,7 @@
5154
TESTFN, rmtree, temp_umask, TESTFN_UNENCODABLE)
5255
from test.support import script_helper
5356
from test.support import threading_helper
54-
from test.test_importlib.util import uncache
57+
from test.test_importlib.util import uncache, temporary_pycache_prefix
5558
from types import ModuleType
5659
try:
5760
import _testsinglephase
@@ -1250,6 +1253,36 @@ class Spec2:
12501253
origin = "a\x00b"
12511254
_imp.create_dynamic(Spec2())
12521255

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

12541287
@skip_if_dont_write_bytecode
12551288
class FilePermissionTests(unittest.TestCase):

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_warnings/data/syntax_warnings.py')
592+
with open(filename, 'rb') as f:
593+
source = f.read()
594+
module_re = r'test\.test_warnings\.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):

Lib/test/test_warnings/__init__.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,77 @@ def test_once(self):
241241
42)
242242
self.assertEqual(len(w), 0)
243243

244+
def test_filter_module(self):
245+
with self.module.catch_warnings(record=True) as w:
246+
self.module.simplefilter('error')
247+
self.module.filterwarnings('always', module=r'package\.module\z')
248+
self.module.warn_explicit('msg', UserWarning, 'filename', 42,
249+
module='package.module')
250+
self.assertEqual(len(w), 1)
251+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module', 42)
252+
self.assertEqual(len(w), 2)
253+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module.py', 42)
254+
self.assertEqual(len(w), 3)
255+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module/__init__.py', 42)
256+
self.assertEqual(len(w), 4)
257+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module/__init__', 42)
258+
self.assertEqual(len(w), 5)
259+
if sys.platform == 'win32':
260+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PY', 42)
261+
self.assertEqual(len(w), 6)
262+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module\__init__.PY', 42)
263+
self.assertEqual(len(w), 6)
264+
265+
with self.module.catch_warnings(record=True) as w:
266+
self.module.simplefilter('error')
267+
self.module.filterwarnings('always', module='package')
268+
self.module.warn_explicit('msg', UserWarning, 'filename', 42,
269+
module='package.module')
270+
self.assertEqual(len(w), 1)
271+
with self.assertRaises(UserWarning):
272+
self.module.warn_explicit('msg', UserWarning, 'filename', 42,
273+
module='other.package.module')
274+
with self.assertRaises(UserWarning):
275+
self.module.warn_explicit('msg', UserWarning, '/path/to/otherpackage/module.py', 42)
276+
277+
with self.module.catch_warnings(record=True) as w:
278+
self.module.simplefilter('error')
279+
self.module.filterwarnings('always', module=r'/path/to/package/module\z')
280+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module', 42)
281+
self.assertEqual(len(w), 1)
282+
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module.py', 42)
283+
self.assertEqual(len(w), 2)
284+
with self.assertRaises(UserWarning):
285+
self.module.warn_explicit('msg', UserWarning, '/PATH/TO/PACKAGE/MODULE', 42)
286+
if sys.platform == 'win32':
287+
self.module.warn_explicit('msg', UserWarning, r'/path/to/package/module.PY', 42)
288+
self.assertEqual(len(w), 3)
289+
with self.assertRaises(UserWarning):
290+
self.module.warn_explicit('msg', UserWarning, r'\path\to\package\module', 42)
291+
292+
if sys.platform == 'win32':
293+
with self.module.catch_warnings(record=True) as w:
294+
self.module.simplefilter('error')
295+
self.module.filterwarnings('always', module=r'C:\\path\\to\\package\\module\z')
296+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module', 42)
297+
self.assertEqual(len(w), 1)
298+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.py', 42)
299+
self.assertEqual(len(w), 2)
300+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PY', 42)
301+
self.assertEqual(len(w), 3)
302+
with self.assertRaises(UserWarning):
303+
self.module.warn_explicit('msg', UserWarning, r'C:\PATH\TO\PACKAGE\MODULE', 42)
304+
with self.assertRaises(UserWarning):
305+
self.module.warn_explicit('msg', UserWarning, r'C:/path/to/package/module', 42)
306+
with self.assertRaises(UserWarning):
307+
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module\__init__', 42)
308+
309+
with self.module.catch_warnings(record=True) as w:
310+
self.module.simplefilter('error')
311+
self.module.filterwarnings('always', module=r'<unknown>\z')
312+
self.module.warn_explicit('msg', UserWarning, '', 42)
313+
self.assertEqual(len(w), 1)
314+
244315
def test_module_globals(self):
245316
with self.module.catch_warnings(record=True) as w:
246317
self.module.simplefilter("always", UserWarning)
@@ -320,7 +391,7 @@ def test_message_matching(self):
320391

321392
def test_mutate_filter_list(self):
322393
class X:
323-
def match(self, a):
394+
def match(self, a, start=0):
324395
L[:] = []
325396

326397
L = [("default",X(),UserWarning,X(),0) for i in range(2)]
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

0 commit comments

Comments
 (0)