Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Doc/library/warnings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,14 @@ Available Functions
ignored.

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

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

.. versionchanged:: next
If no module is passed, test the filter regular expression against
module names created from the path, not only the path itself.


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

Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ unittest
(Contributed by Garry Cairns in :gh:`134567`.)


warnings
--------

* Improve filtering by module in :func:`warnings.warn_explicit` if no *module*
argument is passed.
It now tests the module regular expression in the warnings filter not only
against the filename with ``.py`` stripped, but also against module names
constructed starting from different parent directories of the filename
(with ``/__init__.py``, ``.py`` and, on Windows, ``.pyw`` stripped).
(Contributed by Serhiy Storchaka in :gh:`135801`.)


venv
----

Expand Down
45 changes: 38 additions & 7 deletions Lib/_py_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,20 +520,49 @@ def warn(message, category=None, stacklevel=1, source=None,
)


def _match_filename(pattern, filename, *, MS_WINDOWS=(sys.platform == 'win32')):
if not filename:
return pattern.match('<unknown>') is not None
if filename[0] == '<' and filename[-1] == '>':
return pattern.match(filename) is not None

is_py = (filename[-3:].lower() == '.py'
if MS_WINDOWS else
filename.endswith('.py'))
if is_py:
filename = filename[:-3]
if pattern.match(filename): # for backward compatibility
return True
if MS_WINDOWS:
if is_py and filename[-9:].lower() in (r'\__init__', '/__init__'):
filename = filename[:-9]
elif not is_py and filename[-4:].lower() == '.pyw':
filename = filename[:-4]
filename = filename.replace('\\', '.')
else:
if is_py and filename.endswith('/__init__'):
filename = filename[:-9]
filename = filename.replace('/', '.')
i = 0
while True:
if pattern.match(filename, i):
return True
i = filename.find('.', i) + 1
if not i:
return False


def warn_explicit(message, category, filename, lineno,
module=None, registry=None, module_globals=None,
source=None):
lineno = int(lineno)
if module is None:
module = filename or "<unknown>"
if module[-3:].lower() == ".py":
module = module[:-3] # XXX What about leading pathname?
if isinstance(message, Warning):
text = str(message)
category = message.__class__
else:
text = message
message = category(message)
modules = None
key = (text, category, lineno)
with _wm._lock:
if registry is None:
Expand All @@ -549,9 +578,11 @@ def warn_explicit(message, category, filename, lineno,
action, msg, cat, mod, ln = item
if ((msg is None or msg.match(text)) and
issubclass(category, cat) and
(mod is None or mod.match(module)) and
(ln == 0 or lineno == ln)):
break
(ln == 0 or lineno == ln) and
(mod is None or (_match_filename(mod, filename)
if module is None else
mod.match(module)))):
break
else:
action = _wm.defaultaction
# Early exit actions
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import textwrap
import types
import unittest
import warnings
import weakref
from io import StringIO
from pathlib import Path
Expand Down Expand Up @@ -1124,6 +1125,19 @@ def test_tstring(self):
self.assertIsInstance(tree.body[0].value.values[0], ast.Constant)
self.assertIsInstance(tree.body[0].value.values[1], ast.Interpolation)

def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'<unknown>\z')
ast.parse(source)
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 21])
for wm in wlog:
self.assertEqual(wm.filename, '<unknown>')
self.assertIs(wm.category, SyntaxWarning)


class CopyTests(unittest.TestCase):
"""Test copying and pickling AST nodes."""
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,28 @@ def four_freevars():
three_freevars.__globals__,
closure=my_closure)

def test_exec_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'<string>\z')
exec(source, {})
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
self.assertEqual(wm.filename, '<string>')
self.assertIs(wm.category, SyntaxWarning)

with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=r'<string>\z')
exec(source, {'__name__': 'package.module', '__file__': filename})
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
self.assertEqual(wm.filename, '<string>')
self.assertIs(wm.category, SyntaxWarning)


def test_filter(self):
self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld'))
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def test_run_module_bug1764407(self):
p.stdin.write(b'Timer\n')
p.stdin.write(b'exit()\n')
data = kill_python(p)
self.assertTrue(data.find(b'1 loop') != -1)
self.assertIn(b'1 loop', data)
self.assertTrue(data.find(b'__main__.Timer') != -1)

def test_relativedir_bug46421(self):
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,12 @@ def test_script_as_dev_fd(self):
out, err = p.communicate()
self.assertEqual(out, b"12345678912345678912345\n")

def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
rc, out, err = assert_python_ok('-Werror', '-Walways:::test.test_import.data.syntax_warnings', filename)
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
rc, out, err = assert_python_ok('-Werror', '-Walways:::syntax_warnings', filename)
self.assertEqual(err.count(b': SyntaxWarning: '), 6)


def tearDownModule():
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,20 @@ def test_compile_warning_in_finally(self):
self.assertEqual(wm.category, SyntaxWarning)
self.assertIn("\"is\" with 'int' literal", str(wm.message))

def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
module_re = r'test\.test_import\.data\.syntax_warnings\z'
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=module_re)
compile(source, filename, 'exec')
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)


class TestBooleanExpression(unittest.TestCase):
class Value:
Expand Down
34 changes: 32 additions & 2 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import py_compile
import random
import re
import shutil
import stat
import subprocess
Expand All @@ -23,6 +24,7 @@
import threading
import time
import types
import warnings
import unittest
from unittest import mock
import _imp
Expand Down Expand Up @@ -51,7 +53,7 @@
TESTFN, rmtree, temp_umask, TESTFN_UNENCODABLE)
from test.support import script_helper
from test.support import threading_helper
from test.test_importlib.util import uncache
from test.test_importlib.util import uncache, temporary_pycache_prefix
from types import ModuleType
try:
import _testsinglephase
Expand Down Expand Up @@ -412,7 +414,6 @@ def test_from_import_missing_attr_path_is_canonical(self):
self.assertIsNotNone(cm.exception)

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

def test_filter_syntax_warnings_by_module(self):
module_re = r'test\.test_import\.data\.syntax_warnings\z'
unload('test.test_import.data.syntax_warnings')
with (os_helper.temp_dir() as tmpdir,
temporary_pycache_prefix(tmpdir),
warnings.catch_warnings(record=True) as wlog):
warnings.simplefilter('error')
warnings.filterwarnings('always', module=module_re)
import test.test_import.data.syntax_warnings
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
filename = test.test_import.data.syntax_warnings.__file__
for wm in wlog:
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)

module_re = r'syntax_warnings\z'
unload('test.test_import.data.syntax_warnings')
with (os_helper.temp_dir() as tmpdir,
temporary_pycache_prefix(tmpdir),
warnings.catch_warnings(record=True) as wlog):
warnings.simplefilter('error')
warnings.filterwarnings('always', module=module_re)
import test.test_import.data.syntax_warnings
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
filename = test.test_import.data.syntax_warnings.__file__
for wm in wlog:
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)


@skip_if_dont_write_bytecode
class FilePermissionTests(unittest.TestCase):
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_import/data/syntax_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Syntax warnings emitted in different parts of the Python compiler.

# Parser/lexer/lexer.c
x = 1or 0 # line 4

# Parser/tokenizer/helpers.c
'\z' # line 7

# Parser/string_parser.c
'\400' # line 10

# _PyCompile_Warn() in Python/codegen.c
assert(x, 'message') # line 13
x is 1 # line 14

# _PyErr_EmitSyntaxWarning() in Python/ast_preprocess.c
def f():
try:
pass
finally:
return 42 # line 21
15 changes: 15 additions & 0 deletions Lib/test/test_symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import textwrap
import symtable
import warnings
import unittest

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

def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
with open(filename, 'rb') as f:
source = f.read()
module_re = r'test\.test_import\.data\.syntax_warnings\z'
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
warnings.filterwarnings('always', module=module_re)
symtable.symtable(source, filename, 'exec')
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
for wm in wlog:
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)


class ComprehensionTests(unittest.TestCase):
def get_identifiers_recursive(self, st, res):
Expand Down
29 changes: 19 additions & 10 deletions Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,21 @@ def test_filter_module(self):
self.module.warn_explicit('msg', UserWarning, 'filename', 42,
module='package.module')
self.assertEqual(len(w), 1)
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module', 42)
self.assertEqual(len(w), 2)
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module.py', 42)
self.assertEqual(len(w), 3)
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module/__init__.py', 42)
self.assertEqual(len(w), 4)
with self.assertRaises(UserWarning):
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module', 42)
with self.assertRaises(UserWarning):
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module.py', 42)
self.module.warn_explicit('msg', UserWarning, '/path/to/package/module/__init__', 42)
if MS_WINDOWS:
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PY', 42)
self.assertEqual(len(w), 5)
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module\__INIT__.PY', 42)
self.assertEqual(len(w), 6)
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PYW', 42)
self.assertEqual(len(w), 7)

with self.module.catch_warnings(record=True) as w:
self.module.simplefilter('error')
Expand All @@ -276,9 +287,8 @@ def test_filter_module(self):
with self.assertRaises(UserWarning):
self.module.warn_explicit('msg', UserWarning, '/PATH/TO/PACKAGE/MODULE', 42)
if MS_WINDOWS:
if self.module is py_warnings:
self.module.warn_explicit('msg', UserWarning, r'/path/to/package/module.PY', 42)
self.assertEqual(len(w), 3)
self.module.warn_explicit('msg', UserWarning, r'/path/to/package/module.PY', 42)
self.assertEqual(len(w), 3)
with self.assertRaises(UserWarning):
self.module.warn_explicit('msg', UserWarning, r'/path/to/package/module/__init__.py', 42)
with self.assertRaises(UserWarning):
Expand All @@ -302,9 +312,8 @@ def test_filter_module(self):
self.assertEqual(len(w), 1)
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.py', 42)
self.assertEqual(len(w), 2)
if self.module is py_warnings:
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PY', 42)
self.assertEqual(len(w), 3)
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.PY', 42)
self.assertEqual(len(w), 3)
with self.assertRaises(UserWarning):
self.module.warn_explicit('msg', UserWarning, r'C:\path\to\package\module.pyw', 42)
with self.assertRaises(UserWarning):
Expand Down Expand Up @@ -399,7 +408,7 @@ def test_message_matching(self):

def test_mutate_filter_list(self):
class X:
def match(self, a):
def match(self, a, start=0):
L[:] = []

L = [("default",X(),UserWarning,X(),0) for i in range(2)]
Expand Down
Loading
Loading