Skip to content
Merged
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 @@ -611,6 +611,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
40 changes: 40 additions & 0 deletions Include/internal/pycore_unicodeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,46 @@ extern "C" {


extern int _PyUnicode_IsModifiable(PyObject *unicode);
extern void _PyUnicodeWriter_InitWithBuffer(
_PyUnicodeWriter *writer,
PyObject *buffer);
extern PyObject* _PyUnicode_Result(PyObject *unicode);
extern int _PyUnicode_DecodeUTF8Writer(
_PyUnicodeWriter *writer,
const char *s,
Py_ssize_t size,
_Py_error_handler error_handler,
const char *errors,
Py_ssize_t *consumed);
extern PyObject* _PyUnicode_ResizeCompact(
PyObject *unicode,
Py_ssize_t length);
extern PyObject* _PyUnicode_GetEmpty(void);


/* Generic helper macro to convert characters of different types.
from_type and to_type have to be valid type names, begin and end
are pointers to the source characters which should be of type
"from_type *". to is a pointer of type "to_type *" and points to the
buffer where the result characters are written to. */
#define _PyUnicode_CONVERT_BYTES(from_type, to_type, begin, end, to) \
do { \
to_type *_to = (to_type *)(to); \
const from_type *_iter = (const from_type *)(begin);\
const from_type *_end = (const from_type *)(end);\
Py_ssize_t n = (_end) - (_iter); \
const from_type *_unrolled_end = \
_iter + _Py_SIZE_ROUND_DOWN(n, 4); \
while (_iter < (_unrolled_end)) { \
_to[0] = (to_type) _iter[0]; \
_to[1] = (to_type) _iter[1]; \
_to[2] = (to_type) _iter[2]; \
_to[3] = (to_type) _iter[3]; \
_iter += 4; _to += 4; \
} \
while (_iter < (_end)) \
*_to++ = (to_type) *_iter++; \
} while (0)


static inline void
Expand Down
46 changes: 39 additions & 7 deletions Lib/_py_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,20 +520,50 @@ 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 not is_py and filename[-4:].lower() == '.pyw':
filename = filename[:-4]
is_py = True
if is_py and filename[-9:].lower() in (r'\__init__', '/__init__'):
filename = filename[:-9]
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 +579,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
6 changes: 6 additions & 0 deletions Lib/_pyrepl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ class ColorSpan(NamedTuple):
def str_width(c: str) -> int:
if ord(c) < 128:
return 1
# gh-139246 for zero-width joiner and combining characters
if unicodedata.combining(c):
return 0
category = unicodedata.category(c)
if category == "Cf" and c != "\u00ad":
return 0
w = unicodedata.east_asian_width(c)
if w in ("N", "Na", "H", "A"):
return 1
Expand Down
14 changes: 2 additions & 12 deletions Lib/encodings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import codecs
import sys
from _codecs import _normalize_encoding
from . import aliases

_cache = {}
Expand All @@ -55,18 +56,7 @@ def normalize_encoding(encoding):
if isinstance(encoding, bytes):
encoding = str(encoding, "ascii")

chars = []
punct = False
for c in encoding:
if c.isalnum() or c == '.':
if punct and chars:
chars.append('_')
if c.isascii():
chars.append(c)
punct = False
else:
punct = True
return ''.join(chars)
return _normalize_encoding(encoding)

def search_function(encoding):

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 @@ -1069,6 +1070,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])
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
13 changes: 13 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,19 @@ 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)

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