Skip to content

Commit 6826166

Browse files
gh-135801: Improve filtering by module in warn_explicit() without module argument (GH-140151)
* 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 ".pyw" 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 efc37ba commit 6826166

File tree

13 files changed

+243
-73
lines changed

13 files changed

+243
-73
lines changed

Doc/library/warnings.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,14 @@ Available Functions
487487
ignored.
488488

489489
*module*, if supplied, should be the module name.
490-
If no module is passed, the filename with ``.py`` stripped is used.
490+
If no module is passed, the module regular expression in
491+
:ref:`warnings filter <warning-filter>` will be tested against the module
492+
names constructed from the path components starting from all parent
493+
directories (with ``/__init__.py``, ``.py`` and, on Windows, ``.pyw``
494+
stripped) and against the filename with ``.py`` stripped.
495+
For example, when the filename is ``'/path/to/package/module.py'``, it will
496+
be tested against ``'path.to.package.module'``, ``'to.package.module'``
497+
``'package.module'``, ``'module'``, and ``'/path/to/package/module'``.
491498

492499
*registry*, if supplied, should be the ``__warningregistry__`` dictionary
493500
of the module.
@@ -506,6 +513,10 @@ Available Functions
506513
.. versionchanged:: 3.6
507514
Add the *source* parameter.
508515

516+
.. versionchanged:: next
517+
If no module is passed, test the filter regular expression against
518+
module names created from the path, not only the path itself.
519+
509520

510521
.. function:: showwarning(message, category, filename, lineno, file=None, line=None)
511522

Doc/whatsnew/3.15.rst

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

613613

614+
warnings
615+
--------
616+
617+
* Improve filtering by module in :func:`warnings.warn_explicit` if no *module*
618+
argument is passed.
619+
It now tests the module regular expression in the warnings filter not only
620+
against the filename with ``.py`` stripped, but also against module names
621+
constructed starting from different parent directories of the filename
622+
(with ``/__init__.py``, ``.py`` and, on Windows, ``.pyw`` stripped).
623+
(Contributed by Serhiy Storchaka in :gh:`135801`.)
624+
625+
614626
venv
615627
----
616628

Lib/_py_warnings.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,20 +520,50 @@ 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+
is_py = (filename[-3:].lower() == '.py'
530+
if MS_WINDOWS else
531+
filename.endswith('.py'))
532+
if is_py:
533+
filename = filename[:-3]
534+
if pattern.match(filename): # for backward compatibility
535+
return True
536+
if MS_WINDOWS:
537+
if not is_py and filename[-4:].lower() == '.pyw':
538+
filename = filename[:-4]
539+
is_py = True
540+
if is_py and filename[-9:].lower() in (r'\__init__', '/__init__'):
541+
filename = filename[:-9]
542+
filename = filename.replace('\\', '/')
543+
else:
544+
if is_py and filename.endswith('/__init__'):
545+
filename = filename[:-9]
546+
filename = filename.replace('/', '.')
547+
i = 0
548+
while True:
549+
if pattern.match(filename, i):
550+
return True
551+
i = filename.find('.', i) + 1
552+
if not i:
553+
return False
554+
555+
523556
def warn_explicit(message, category, filename, lineno,
524557
module=None, registry=None, module_globals=None,
525558
source=None):
526559
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?
531560
if isinstance(message, Warning):
532561
text = str(message)
533562
category = message.__class__
534563
else:
535564
text = message
536565
message = category(message)
566+
modules = None
537567
key = (text, category, lineno)
538568
with _wm._lock:
539569
if registry is None:
@@ -549,9 +579,11 @@ def warn_explicit(message, category, filename, lineno,
549579
action, msg, cat, mod, ln = item
550580
if ((msg is None or msg.match(text)) and
551581
issubclass(category, cat) and
552-
(mod is None or mod.match(module)) and
553-
(ln == 0 or lineno == ln)):
554-
break
582+
(ln == 0 or lineno == ln) and
583+
(mod is None or (_match_filename(mod, filename)
584+
if module is None else
585+
mod.match(module)))):
586+
break
555587
else:
556588
action = _wm.defaultaction
557589
# 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
@@ -1069,6 +1070,19 @@ def test_tstring(self):
10691070
self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
10701071
self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)
10711072

1073+
def test_filter_syntax_warnings_by_module(self):
1074+
filename = support.findfile('test_import/data/syntax_warnings.py')
1075+
with open(filename, 'rb') as f:
1076+
source = f.read()
1077+
with warnings.catch_warnings(record=True) as wlog:
1078+
warnings.simplefilter('error')
1079+
warnings.filterwarnings('always', module=r'<unknown>\z')
1080+
ast.parse(source)
1081+
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
1082+
for wm in wlog:
1083+
self.assertEqual(wm.filename, '<unknown>')
1084+
self.assertIs(wm.category, SyntaxWarning)
1085+
10721086

10731087
class CopyTests(unittest.TestCase):
10741088
"""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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,19 @@ 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(
816+
'-Werror',
817+
'-Walways:::test.test_import.data.syntax_warnings',
818+
filename)
819+
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
820+
821+
rc, out, err = assert_python_ok(
822+
'-Werror',
823+
'-Walways:::syntax_warnings',
824+
filename)
825+
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
813826

814827

815828
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
@support.subTests('src', [
17491763
textwrap.dedent("""
17501764
def f():

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)