From cbd66f6fab5504767656d31745ecde6a1cb7ed35 Mon Sep 17 00:00:00 2001 From: Joannah Nanjekye Date: Thu, 16 Jun 2022 18:28:57 +0300 Subject: [PATCH] Add 2.x warning --- Doc/c-api/exceptions.rst | 18 + Doc/library/test.rst | 12 + Doc/library/warnings.rst | 40 ++- Include/cpython/warnings.h | 9 + Include/internal/pycore_global_strings.h | 2 + Include/internal/pycore_runtime_init.h | 2 + Include/warnings.h | 18 + Lib/logging/__init__.py | 20 ++ Lib/test/support/warnings_helper.py | 26 ++ Lib/test/test_py2xwarn.py | 15 + Lib/test/test_warnings/__init__.py | 23 +- Lib/warnings.py | 226 +++++++++++- Python/_warnings.c | 438 ++++++++++++++++++++++- Python/sysmodule.c | 1 + Tools/c-analyzer/cpython/ignored.tsv | 1 + Tools/scripts/generate_global_objects.py | 2 + 16 files changed, 845 insertions(+), 8 deletions(-) create mode 100644 Lib/test/test_py2xwarn.py diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 180965a90bae0f..9ff4b746bdb6a7 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -339,6 +339,14 @@ an error value). .. versionadded:: 3.4 +.. c:function:: int PyErr_WarnExplicitWithFixObject(PyObject *category, PyObject *message, PyObject *fix, PyObject *filename, int lineno, PyObject *module, PyObject *registry) + + Issue a warning message and fix with explicit control over all warning attributes. This + is a straightforward wrapper around the Python function + :func:`warnings.warnings_warn_explicit_with_fix`; see there for more information. The *module* + and *registry* arguments may be set to ``NULL`` to get the default effect + described there. + .. c:function:: int PyErr_WarnExplicit(PyObject *category, const char *message, const char *filename, int lineno, const char *module, PyObject *registry) @@ -347,6 +355,11 @@ an error value). :term:`filesystem encoding and error handler`. +.. c:function:: int PyErr_WarnExplicit_WithFix(PyObject *category, const char *message, const char *fix, const char *filename, int lineno, const char *module, PyObject *registry) + + Similar to :c:func:`PyErr_WarnExplicit` with an additional *fix* parameter. + + .. c:function:: int PyErr_WarnFormat(PyObject *category, Py_ssize_t stack_level, const char *format, ...) Function similar to :c:func:`PyErr_WarnEx`, but use @@ -363,6 +376,11 @@ an error value). .. versionadded:: 3.6 +.. c:function:: int PyErr_WarnPy2x(char *message, char *fix, int stacklevel) + + Issue a :exc:`DeprecationWarning` with the given *message*, *fix* and *stacklevel* + if the :c:data:`Py_Py2xWarningFlag` flag is enabled. + Querying the error indicator ============================ diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 707e966455ceb5..b77c0152cbf5e3 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -526,6 +526,18 @@ The :mod:`test.support` module defines the following functions: Return a list of command line arguments reproducing the current optimization settings in ``sys.flags``. +.. function:: check_py2x_warnings(*filters, quiet=False) + + Similar to :func:`check_warnings`, but for Python 3 compatibility warnings. + If ``sys.py3xwarning == 1``, it checks if the warning is effectively raised. + If ``sys.py3xwarning == 0``, it checks that no warning is raised. It + accepts 2-tuples of the form ``("message regexp", WarningCategory)`` as + positional arguments. When the optional keyword argument *quiet* is + :const:`True`, it does not fail if a filter catches nothing. Without + arguments, it defaults to:: + + check_py2x_warnings(("", DeprecationWarning), quiet=False) + .. function:: captured_stdin() captured_stdout() diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 5b2222ec2cebc0..1d448e9942d1f5 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -419,7 +419,6 @@ Available Functions .. versionchanged:: 3.6 Added *source* parameter. - .. function:: warn_explicit(message, category, filename, lineno, module=None, registry=None, module_globals=None, source=None) This is a low-level interface to the functionality of :func:`warn`, passing in @@ -439,6 +438,25 @@ Available Functions *source*, if supplied, is the destroyed object which emitted a :exc:`ResourceWarning`. +.. function:: warn_explicit_with_fix(message, fix, category, filename, lineno, module=None, registry=None, module_globals=None, source=None) + + This is a low-level interface to the functionality of :func:`warn`, passing in + explicitly the message, fix, category, filename and line number, and optionally the + module name and the registry (which should be the ``__warningregistry__`` + dictionary of the module). The module name defaults to the filename with + ``.py`` stripped; if no registry is passed, the warning is never suppressed. + *message* must be a string and *category* a subclass of :exc:`Warning` or + *message* may be a :exc:`Warning` instance, in which case *category* will be + ignored. + + *module_globals*, if supplied, should be the global namespace in use by the code + for which the warning is issued. (This argument is used to support displaying + source for modules found in zipfiles or other non-filesystem import + sources). + + *source*, if supplied, is the destroyed object which emitted a + :exc:`ResourceWarning`. + .. versionchanged:: 3.6 Add the *source* parameter. @@ -454,6 +472,17 @@ Available Functions try to read the line specified by *filename* and *lineno*. +.. function:: showwarningwithfix(message, fix, category, filename, lineno, file=None, line=None) + + Write a warning to a file. The default implementation calls + ``formatwarningwithfix(message, fix, category, filename, lineno, line)`` and writes the + resulting string to *file*, which defaults to :data:`sys.stderr`. You may replace + this function with any callable by assigning to ``warnings.showwarning``. + *line* is a line of source code to be included in the warning + message; if *line* is not supplied, :func:`showwarning` will + try to read the line specified by *filename* and *lineno*. + + .. function:: formatwarning(message, category, filename, lineno, line=None) Format a warning the standard way. This returns a string which may contain @@ -463,6 +492,15 @@ Available Functions *lineno*. +.. function:: formatwarningwithfix(message, fix, category, filename, lineno, line=None) + + Format a warning the standard way. This returns a string which may contain + embedded newlines and ends in a newline. *line* is a line of source code to + be included in the warning message; if *line* is not supplied, + :func:`formatwarning` will try to read the line specified by *filename* and + *lineno*. + + .. function:: filterwarnings(action, message='', category=Warning, module='', lineno=0, append=False) Insert an entry into the list of :ref:`warnings filter specifications diff --git a/Include/cpython/warnings.h b/Include/cpython/warnings.h index 2ef8e3ce9435f4..8eebf0e35533d4 100644 --- a/Include/cpython/warnings.h +++ b/Include/cpython/warnings.h @@ -10,6 +10,15 @@ PyAPI_FUNC(int) PyErr_WarnExplicitObject( PyObject *module, PyObject *registry); +PyAPI_FUNC(int) PyErr_WarnExplicitWithFixObject( + PyObject *category, + PyObject *message, + PyObject *fix, + PyObject *filename, + int lineno, + PyObject *module, + PyObject *registry); + PyAPI_FUNC(int) PyErr_WarnExplicitFormat( PyObject *category, const char *filename, int lineno, diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index cfa8ae99d1b6d9..fefd55f471667e 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -57,6 +57,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(TextIOWrapper) STRUCT_FOR_ID(True) STRUCT_FOR_ID(WarningMessage) + STRUCT_FOR_ID(WarningMessageAndFix) STRUCT_FOR_ID(_) STRUCT_FOR_ID(__IOBase_closed) STRUCT_FOR_ID(__abc_tpflags__) @@ -221,6 +222,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_is_text_encoding) STRUCT_FOR_ID(_lock_unlock_module) STRUCT_FOR_ID(_showwarnmsg) + STRUCT_FOR_ID(_showwarnmsgwithfix) STRUCT_FOR_ID(_shutdown) STRUCT_FOR_ID(_slotnames) STRUCT_FOR_ID(_strptime_time) diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index 737507f07eacce..0ce8f7d4b79501 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -679,6 +679,7 @@ extern "C" { INIT_ID(TextIOWrapper), \ INIT_ID(True), \ INIT_ID(WarningMessage), \ + INIT_ID(WarningMessageAndFix), \ INIT_ID(_), \ INIT_ID(__IOBase_closed), \ INIT_ID(__abc_tpflags__), \ @@ -843,6 +844,7 @@ extern "C" { INIT_ID(_is_text_encoding), \ INIT_ID(_lock_unlock_module), \ INIT_ID(_showwarnmsg), \ + INIT_ID(_showwarnmsgwithfix), \ INIT_ID(_shutdown), \ INIT_ID(_slotnames), \ INIT_ID(_strptime_time), \ diff --git a/Include/warnings.h b/Include/warnings.h index 18ac1543a3ca9e..6397c42126ded6 100644 --- a/Include/warnings.h +++ b/Include/warnings.h @@ -9,6 +9,12 @@ PyAPI_FUNC(int) PyErr_WarnEx( const char *message, /* UTF-8 encoded string */ Py_ssize_t stack_level); +PyAPI_FUNC(int) PyErr_WarnEx_WithFix( + PyObject *category, + const char *message, /* UTF-8 encoded string */ + const char *fix, /* UTF-8 encoded string */ + Py_ssize_t stack_level); + PyAPI_FUNC(int) PyErr_WarnFormat( PyObject *category, Py_ssize_t stack_level, @@ -32,6 +38,18 @@ PyAPI_FUNC(int) PyErr_WarnExplicit( const char *module, /* UTF-8 encoded string */ PyObject *registry); +PyAPI_FUNC(int) PyErr_WarnExplicit_WithFix( + PyObject *category, + const char *message, /* UTF-8 encoded string */ + const char *fix, /* UTF-8 encoded string */ + const char *filename, /* decoded from the filesystem encoding */ + int lineno, + const char *module, /* UTF-8 encoded string */ + PyObject *registry); + +#define PyErr_WarnPy2x(msg, fix, stacklevel) \ + (Py_Py2xWarningFlag ? PyErr_WarnEx_WithFix(PyExc_DeprecationWarning, msg, fix, stacklevel) : 0) + #ifndef Py_LIMITED_API # define Py_CPYTHON_WARNINGS_H # include "cpython/warnings.h" diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 79e4b7d09bfa43..394477973511f5 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -2256,6 +2256,26 @@ def _showwarning(message, category, filename, lineno, file=None, line=None): # since some log aggregation tools group logs by the msg arg logger.warning(str(s)) +def _showwarningwithfix(message, fix, category, filename, lineno, file=None, line=None): + """ + Implementation of showwarnings which redirects to logging, which will first + check to see if the file parameter is None. If a file is specified, it will + delegate to the original warnings implementation of showwarning. Otherwise, + it will call warnings.formatwarningwithfix and will log the resulting string to a + warnings logger named "py.warnings" with level logging.WARNING. + """ + if file is not None: + if _warnings_showwarningwithfix is not None: + _warnings_showwarningwithfix(message, fix, category, filename, lineno, file, line) + else: + s = warnings.formatwarningwithfix(message, fix, category, filename, lineno, line) + logger = getLogger("py.warnings") + if not logger.handlers: + logger.addHandler(NullHandler()) + # bpo-46557: Log str(s) as msg instead of logger.warning("%s", s) + # since some log aggregation tools group logs by the msg arg + logger.warning(str(s)) + def captureWarnings(capture): """ If capture is true, redirect all warnings to the logging package. diff --git a/Lib/test/support/warnings_helper.py b/Lib/test/support/warnings_helper.py index 28e96f88b24441..bb771597eb7525 100644 --- a/Lib/test/support/warnings_helper.py +++ b/Lib/test/support/warnings_helper.py @@ -72,8 +72,11 @@ def __getattr__(self, attr): return getattr(self._warnings[-1], attr) elif attr in warnings.WarningMessage._WARNING_DETAILS: return None + elif attr in warnings.WarningMessageAndFix._WARNING_DETAILS: + return None raise AttributeError("%r has no attribute %r" % (self, attr)) + @property def warnings(self): return self._warnings[self._last:] @@ -106,6 +109,29 @@ def check_warnings(*filters, **kwargs): return _filterwarnings(filters, quiet) +@contextlib.contextmanager +def check_py2x_warnings(*filters, **kwargs): + """Context manager to silence py2x warnings. + + Accept 2-tuples as positional arguments: + ("message regexp", WarningCategory) + + Optional argument: + - if 'quiet' is True, it does not fail if a filter catches nothing + (default False) + + Without argument, it defaults to: + check_py2x_warnings(("", DeprecationWarning), quiet=False) + """ + if sys.py2x_warning: + if not filters: + filters = (("", DeprecationWarning),) + else: + # It should not raise any py2x warning + filters = () + return _filterwarnings(filters, kwargs.get('quiet')) + + @contextlib.contextmanager def check_no_warnings(testcase, message='', category=Warning, force_gc=False): """Context manager to check that no warnings are emitted. diff --git a/Lib/test/test_py2xwarn.py b/Lib/test/test_py2xwarn.py new file mode 100644 index 00000000000000..e802da1c722f5d --- /dev/null +++ b/Lib/test/test_py2xwarn.py @@ -0,0 +1,15 @@ +import unittest +import sys +from test.support.warnings_helper import check_py2x_warnings +import warnings + +if not sys.py2x_warning: + raise unittest.SkipTest('%s must be run with the -2 flag' % __name__) + +class TestPy2xWarnings(unittest.TestCase): + + def assertWarning(self, _, warning, expected_message): + self.assertEqual(str(warning.message), expected_message) + + def assertNoWarning(self, _, recorder): + self.assertEqual(len(recorder.warnings), 0) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 0f960b82bfaebc..5b4f558df1b3e3 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -89,8 +89,8 @@ class PublicAPITests(BaseTest): def test_module_all_attribute(self): self.assertTrue(hasattr(self.module, '__all__')) - target_api = ["warn", "warn_explicit", "showwarning", - "formatwarning", "filterwarnings", "simplefilter", + target_api = ["warn", "warn_explicit", "warn_explicit_with_fix", "showwarning", "showwarningwithfix", + "formatwarning", "formatwarningwithfix", "filterwarnings", "simplefilter", "resetwarnings", "catch_warnings"] self.assertSetEqual(set(self.module.__all__), set(target_api)) @@ -904,6 +904,25 @@ def test_formatwarning(self): self.assertEqual(expect, self.module.formatwarning(message, category, file_name, line_num, file_line)) + def test_formatwarningwithfix(self): + message = "msg" + fix = "fix" + category = Warning + file_name = os.path.splitext(warning_tests.__file__)[0] + '.py' + line_num = 3 + file_line = linecache.getline(file_name, line_num).strip() + format = "%s:%s: %s: %s: %s\n %s\n" + expect = format % (file_name, line_num, category.__name__, message, fix, + file_line) + self.assertEqual(expect, self.module.formatwarningwithfix(message, fix, + category, file_name, line_num)) + # Test the 'line' argument. + file_line += " for the win!" + expect = format % (file_name, line_num, category.__name__, message, fix, + file_line) + self.assertEqual(expect, self.module.formatwarningwithfix(message, fix, + category, file_name, line_num, file_line)) + def test_showwarning(self): file_name = os.path.splitext(warning_tests.__file__)[0] + '.py' line_num = 3 diff --git a/Lib/warnings.py b/Lib/warnings.py index 7d8c4400127f7f..511c5b7275e919 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -3,20 +3,30 @@ import sys -__all__ = ["warn", "warn_explicit", "showwarning", - "formatwarning", "filterwarnings", "simplefilter", - "resetwarnings", "catch_warnings"] +__all__ = ["warn", "warn_explicit", "warn_explicit_with_fix", "showwarning", + "formatwarning", "formatwarningwithfix", "filterwarnings", "simplefilter", + "resetwarnings", "showwarningwithfix", "catch_warnings"] def showwarning(message, category, filename, lineno, file=None, line=None): """Hook to write a warning to a file; replace if you like.""" msg = WarningMessage(message, category, filename, lineno, file, line) _showwarnmsg_impl(msg) +def showwarningwithfix(message, fix, category, filename, lineno, file=None, line=None): + """Hook to write a warning to a file; replace if you like.""" + msg = WarningMessageAndFix(message, fix, category, filename, lineno, file, line) + _showwarnmsgwithfix_impl(msg) + def formatwarning(message, category, filename, lineno, line=None): """Function to format a warning the standard way.""" msg = WarningMessage(message, category, filename, lineno, None, line) return _formatwarnmsg_impl(msg) +def formatwarningwithfix(message, fix, category, filename, lineno, line=None): + """Function to format a warning the standard way.""" + msg = WarningMessageAndFix(message, fix, category, filename, lineno, None, line) + return _formatwarnmsgwithfix_impl(msg) + def _showwarnmsg_impl(msg): file = msg.file if file is None: @@ -32,6 +42,21 @@ def _showwarnmsg_impl(msg): # the file (probably stderr) is invalid - this warning gets lost. pass +def _showwarnmsgwithfix_impl(msg): + file = msg.file + if file is None: + file = sys.stderr + if file is None: + # sys.stderr is None when run with pythonw.exe: + # warnings get lost + return + text = _formatwarnmsgwithfix(msg) + try: + file.write(text) + except OSError: + # the file (probably stderr) is invalid - this warning gets lost. + pass + def _formatwarnmsg_impl(msg): category = msg.category.__name__ s = f"{msg.filename}:{msg.lineno}: {category}: {msg.message}\n" @@ -90,6 +115,64 @@ def _formatwarnmsg_impl(msg): f'allocation traceback\n') return s +def _formatwarnmsgwithfix_impl(msg): + category = msg.category.__name__ + s = f"{msg.filename}:{msg.lineno}: {category}: {msg.message}: {msg.fix}\n" + + if msg.line is None: + try: + import linecache + line = linecache.getline(msg.filename, msg.lineno) + except Exception: + # When a warning is logged during Python shutdown, linecache + # and the import machinery don't work anymore + line = None + linecache = None + else: + line = msg.line + if line: + line = line.strip() + s += " %s\n" % line + + if msg.source is not None: + try: + import tracemalloc + # Logging a warning should not raise a new exception: + # catch Exception, not only ImportError and RecursionError. + except Exception: + # don't suggest to enable tracemalloc if it's not available + tracing = True + tb = None + else: + tracing = tracemalloc.is_tracing() + try: + tb = tracemalloc.get_object_traceback(msg.source) + except Exception: + # When a warning is logged during Python shutdown, tracemalloc + # and the import machinery don't work anymore + tb = None + + if tb is not None: + s += 'Object allocated at (most recent call last):\n' + for frame in tb: + s += (' File "%s", lineno %s\n' + % (frame.filename, frame.lineno)) + + try: + if linecache is not None: + line = linecache.getline(frame.filename, frame.lineno) + else: + line = None + except Exception: + line = None + if line: + line = line.strip() + s += ' %s\n' % line + elif not tracing: + s += (f'{category}: Enable tracemalloc to get the object ' + f'allocation traceback\n') + return s + # Keep a reference to check if the function was replaced _showwarning_orig = showwarning @@ -111,6 +194,27 @@ def _showwarnmsg(msg): return _showwarnmsg_impl(msg) +# Keep a reference to check if the function was replaced +_showwarningwithfix_orig = showwarningwithfix + +def _showwarnmsgwithfix(msg): + """Hook to write a warning to a file; replace if you like.""" + try: + sw = showwarningwithfix + except NameError: + pass + else: + if sw is not _showwarningwithfix_orig: + # warnings.showwarningwithfix() was replaced + if not callable(sw): + raise TypeError("warnings.showwarningwithfix() must be set to a " + "function or method") + + sw(msg.message, msg.fix, msg.category, msg.filename, msg.lineno, + msg.file, msg.line) + return + _showwarnmsgwithfix_impl(msg) + # Keep a reference to check if the function was replaced _formatwarning_orig = formatwarning @@ -127,6 +231,21 @@ def _formatwarnmsg(msg): msg.filename, msg.lineno, msg.line) return _formatwarnmsg_impl(msg) +_formatwarningwithfix_orig = formatwarningwithfix + +def _formatwarnmsgwithfix(msg): + """Function to format a warning the standard way.""" + try: + fw = formatwarningwithfix + except NameError: + pass + else: + if fw is not _formatwarningwithfix_orig: + # warnings.formatwarningwithfix() was replaced + return fw(msg.message, msg.fix, msg.category, + msg.filename, msg.lineno, msg.line) + return _formatwarnmsgwithfix_impl(msg) + def filterwarnings(action, message="", category=Warning, module="", lineno=0, append=False): """Insert an entry into the list of warnings filters (at the front). @@ -394,6 +513,77 @@ def warn_explicit(message, category, filename, lineno, msg = WarningMessage(message, category, filename, lineno, source) _showwarnmsg(msg) +def warn_explicit_with_fix(message, fix, category, filename, lineno, + module=None, registry=None, module_globals=None, + source=None): + lineno = int(lineno) + if module is None: + module = filename or "" + if module[-3:].lower() == ".py": + module = module[:-3] # XXX What about leading pathname? + if registry is None: + registry = {} + if registry.get('version', 0) != _filters_version: + registry.clear() + registry['version'] = _filters_version + if isinstance(message, Warning): + text = str(message) + category = message.__class__ + else: + text = message + message = category(message) + key = (text, category, lineno) + # Quick test for common case + if registry.get(key): + return + # Search the filters + for item in filters: + 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 + else: + action = defaultaction + # Early exit actions + if action == "ignore": + return + + # Prime the linecache for formatting, in case the + # "file" is actually in a zipfile or something. + import linecache + linecache.getlines(filename, module_globals) + + if action == "error": + raise message + # Other actions + if action == "once": + registry[key] = 1 + oncekey = (text, category) + if onceregistry.get(oncekey): + return + onceregistry[oncekey] = 1 + elif action == "always": + pass + elif action == "module": + registry[key] = 1 + altkey = (text, category, 0) + if registry.get(altkey): + return + registry[altkey] = 1 + elif action == "default": + registry[key] = 1 + else: + # Unrecognized actions are errors + raise RuntimeError( + "Unrecognized action (%r) in warnings.filters:\n %s" % + (action, item)) + # Print message, fix and context + msg = WarningMessageAndFix(message, fix, category, filename, lineno, source) + _showwarnmsgwithfix(msg) + + class WarningMessage(object): @@ -416,6 +606,28 @@ def __str__(self): "line : %r}" % (self.message, self._category_name, self.filename, self.lineno, self.line)) +class WarningMessageAndFix(object): + + _WARNING_DETAILS = ("message", "fix", "category", "filename", "lineno", "file", + "line", "source") + + def __init__(self, message, fix, category, filename, lineno, file=None, + line=None, source=None): + self.message = message + self.fix = fix + self.category = category + self.filename = filename + self.lineno = lineno + self.file = file + self.line = line + self.source = source + self._category_name = category.__name__ if category else None + + def __str__(self): + return ("{message : %r, fix : %r, category : %r, filename : %r, lineno : %s, " + "line : %r}" % (self.message, self._category_name, + self.filename, self.lineno, self.line)) + class catch_warnings(object): @@ -472,14 +684,18 @@ def __enter__(self): self._module._filters_mutated() self._showwarning = self._module.showwarning self._showwarnmsg_impl = self._module._showwarnmsg_impl + self._showwarningwithfix = self._module.showwarningwithfix + self._showwarnmsgwithfix_impl = self._module._showwarnmsgwithfix_impl if self._filter is not None: simplefilter(*self._filter) if self._record: log = [] self._module._showwarnmsg_impl = log.append + self._module._showwarnmsgwithfix_impl = log.append # Reset showwarning() to the default implementation to make sure # that _showwarnmsg() calls _showwarnmsg_impl() self._module.showwarning = self._module._showwarning_orig + self._module.showwarningwithfix = self._module._showwarningwithfix_orig return log else: return None @@ -491,6 +707,8 @@ def __exit__(self, *exc_info): self._module._filters_mutated() self._module.showwarning = self._showwarning self._module._showwarnmsg_impl = self._showwarnmsg_impl + self._module.showwarningwithfix = self._showwarningwithfix + self._module._showwarnmsgwithfix_impl = self._showwarnmsgwithfix_impl _DEPRECATED_MSG = "{name!r} is deprecated and slated for removal in Python {remove}" @@ -547,7 +765,7 @@ def extract(): # If either if the compiled regexs are None, match anything. try: from _warnings import (filters, _defaultaction, _onceregistry, - warn, warn_explicit, _filters_mutated) + warn, warn_explicit, warn_explicit_with_fix, _filters_mutated) defaultaction = _defaultaction onceregistry = _onceregistry _warnings_defaults = True diff --git a/Python/_warnings.c b/Python/_warnings.c index 942308b357e338..dd7cf149725852 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -9,6 +9,7 @@ #include "clinic/_warnings.c.h" #define MODULE_NAME "_warnings" +#define ACTION_NAME "always" PyDoc_STRVAR(warnings__doc__, MODULE_NAME " provides basic warning filtering support.\n" @@ -622,6 +623,140 @@ call_show_warning(PyThreadState *tstate, PyObject *category, return -1; } +static void +show_warning_with_fix(PyThreadState *tstate, PyObject *filename, int lineno, + PyObject *text, PyObject *fixtxt, PyObject *category, PyObject *sourceline) +{ + PyObject *f_stderr; + PyObject *name; + char lineno_str[128]; + + PyOS_snprintf(lineno_str, sizeof(lineno_str), ":%d: ", lineno); + + name = PyObject_GetAttr(category, &_Py_ID(__name__)); + if (name == NULL) { + goto error; + } + + f_stderr = _PySys_GetAttr(tstate, &_Py_ID(stderr)); + if (f_stderr == NULL) { + fprintf(stderr, "lost sys.stderr\n"); + goto error; + } + + /* Print "filename:lineno: category: text\n" */ + if (PyFile_WriteObject(filename, f_stderr, Py_PRINT_RAW) < 0) + goto error; + if (PyFile_WriteString(lineno_str, f_stderr) < 0) + goto error; + if (PyFile_WriteObject(name, f_stderr, Py_PRINT_RAW) < 0) + goto error; + if (PyFile_WriteString(": ", f_stderr) < 0) + goto error; + if (PyFile_WriteObject(text, f_stderr, Py_PRINT_RAW) < 0) + goto error; + if (PyFile_WriteString(": ", f_stderr) < 0) + goto error; + if (PyFile_WriteObject(fixtxt, f_stderr, Py_PRINT_RAW) < 0) + goto error; + if (PyFile_WriteString("\n", f_stderr) < 0) + goto error; + Py_CLEAR(name); + + /* Print " source_line\n" */ + if (sourceline) { + int kind; + const void *data; + Py_ssize_t i, len; + Py_UCS4 ch; + PyObject *truncated; + + if (PyUnicode_READY(sourceline) < 1) + goto error; + + kind = PyUnicode_KIND(sourceline); + data = PyUnicode_DATA(sourceline); + len = PyUnicode_GET_LENGTH(sourceline); + for (i=0; iinterp; + + /* If the source parameter is set, try to get the Python implementation. + The Python implementation is able to log the traceback where the source + was allocated, whereas the C implementation doesn't. */ + show_fn = GET_WARNINGS_ATTR(interp, _showwarnmsgwithfix, source != NULL); + if (show_fn == NULL) { + if (PyErr_Occurred()) + return -1; + show_warning_with_fix(tstate, filename, lineno, text, fix_txt, category, sourceline); + return 0; + } + + if (!PyCallable_Check(show_fn)) { + PyErr_SetString(PyExc_TypeError, + "warnings._showwarnmsgwithfix() must be set to a callable"); + goto error; + } + + warnmsg_cls = GET_WARNINGS_ATTR(interp, WarningMessageAndFix, 0); + if (warnmsg_cls == NULL) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_RuntimeError, + "unable to get warnings.WarningMessageAndFix"); + } + goto error; + } + + msg = PyObject_CallFunctionObjArgs(warnmsg_cls, message, fix, category, + filename, lineno_obj, Py_None, Py_None, source, + NULL); + Py_DECREF(warnmsg_cls); + if (msg == NULL) + goto error; + + res = PyObject_CallOneArg(show_fn, msg); + Py_DECREF(show_fn); + Py_DECREF(msg); + + if (res == NULL) + return -1; + + Py_DECREF(res); + return 0; + +error: + Py_XDECREF(show_fn); + return -1; +} + static PyObject * warn_explicit(PyThreadState *tstate, PyObject *category, PyObject *message, PyObject *filename, int lineno, @@ -712,7 +847,7 @@ warn_explicit(PyThreadState *tstate, PyObject *category, PyObject *message, /* Store in the registry that we've been here, *except* when the action is "always". */ rc = 0; - if (!_PyUnicode_EqualToASCIIString(action, "always")) { + if (!_PyUnicode_EqualToASCIIString(action, ACTION_NAME)) { if (registry != NULL && registry != Py_None && PyDict_SetItem(registry, key, Py_True) < 0) { @@ -765,6 +900,151 @@ warn_explicit(PyThreadState *tstate, PyObject *category, PyObject *message, return result; /* Py_None or NULL. */ } +static PyObject * +warn_explicit_with_fix(PyThreadState *tstate, PyObject *category, PyObject *message, + PyObject *fix, PyObject *filename, int lineno, + PyObject *module, PyObject *registry, PyObject *sourceline, + PyObject *source) +{ + PyObject *key = NULL, *text = NULL, *fix_txt = NULL, *result = NULL, *lineno_obj = NULL; + PyObject *item = NULL; + PyObject *action; + int rc; + PyInterpreterState *interp = tstate->interp; + + /* module can be None if a warning is emitted late during Python shutdown. + In this case, the Python warnings module was probably unloaded, filters + are no more available to choose as action. It is safer to ignore the + warning and do nothing. */ + if (module == Py_None) + Py_RETURN_NONE; + + if (registry && !PyDict_Check(registry) && (registry != Py_None)) { + PyErr_SetString(PyExc_TypeError, "'registry' must be a dict or None"); + return NULL; + } + + /* Normalize module. */ + if (module == NULL) { + module = normalize_module(filename); + if (module == NULL) + return NULL; + } + else + Py_INCREF(module); + + /* Normalize message. */ + Py_INCREF(message); + Py_INCREF(fix); /* DECREF'ed in cleanup. */ + fix_txt = fix; + rc = PyObject_IsInstance(message, PyExc_Warning); + if (rc == -1) { + goto cleanup; + } + if (rc == 1) { + text = PyObject_Str(message); + if (text == NULL) + goto cleanup; + category = (PyObject*)Py_TYPE(message); + } + else { + text = message; + message = PyObject_CallOneArg(category, message); + if (message == NULL) + goto cleanup; + } + + lineno_obj = PyLong_FromLong(lineno); + if (lineno_obj == NULL) + goto cleanup; + + if (source == Py_None) { + source = NULL; + } + + /* Create key. */ + key = PyTuple_Pack(4, text, fix_txt, category, lineno_obj); + if (key == NULL) + goto cleanup; + + if ((registry != NULL) && (registry != Py_None)) { + rc = already_warned(interp, registry, key, 0); + if (rc == -1) + goto cleanup; + else if (rc == 1) + goto return_none; + /* Else this warning hasn't been generated before. */ + } + + action = get_filter(interp, category, text, lineno, module, &item); + if (action == NULL) + goto cleanup; + + if (_PyUnicode_EqualToASCIIString(action, "error")) { + PyErr_SetObject(category, message); + goto cleanup; + } + + if (_PyUnicode_EqualToASCIIString(action, "ignore")) { + goto return_none; + } + + /* Store in the registry that we've been here, *except* when the action + is "always". */ + rc = 0; + if (!_PyUnicode_EqualToASCIIString(action, ACTION_NAME)) { + if (registry != NULL && registry != Py_None && + PyDict_SetItem(registry, key, Py_True) < 0) + { + goto cleanup; + } + + if (_PyUnicode_EqualToASCIIString(action, "once")) { + if (registry == NULL || registry == Py_None) { + registry = get_once_registry(interp); + if (registry == NULL) + goto cleanup; + } + /* WarningsState.once_registry[(text, category)] = 1 */ + rc = update_registry(interp, registry, text, category, 0); + } + else if (_PyUnicode_EqualToASCIIString(action, "module")) { + /* registry[(text, category, 0)] = 1 */ + if (registry != NULL && registry != Py_None) + rc = update_registry(interp, registry, text, category, 0); + } + else if (!_PyUnicode_EqualToASCIIString(action, "default")) { + PyErr_Format(PyExc_RuntimeError, + "Unrecognized action (%R) in warnings.filters:\n %R", + action, item); + goto cleanup; + } + } + + if (rc == 1) /* Already warned for this module. */ + goto return_none; + if (rc == -1 || (rc == 0 && call_show_warning_with_fix(tstate, category, text, + message, fix_txt, fix, filename, + lineno, lineno_obj, sourceline, source) < 0)) { + goto cleanup; + } + + return_none: + result = Py_None; + Py_INCREF(result); + + cleanup: + Py_XDECREF(item); + Py_XDECREF(key); + Py_XDECREF(text); + Py_XDECREF(fix_txt); + Py_XDECREF(lineno_obj); + Py_DECREF(module); + Py_XDECREF(message); + Py_XDECREF(fix); + return result; /* Py_None or NULL. */ +} + static int is_internal_frame(PyFrameObject *frame) { @@ -930,6 +1210,29 @@ get_category(PyObject *message, PyObject *category) return category; } +static PyObject * +do_warn_with_fix(PyObject *message, PyObject *fix, PyObject *category, + Py_ssize_t stack_level, PyObject *source) +{ + PyObject *filename, *module, *registry, *res; + int lineno; + + PyThreadState *tstate = get_current_tstate(); + if (tstate == NULL) { + return NULL; + } + + if (!setup_context(stack_level, &filename, &lineno, &module, ®istry)) + return NULL; + + res = warn_explicit_with_fix(tstate, category, message, fix, filename, lineno, module, registry, + NULL, source); + Py_DECREF(filename); + Py_DECREF(registry); + Py_DECREF(module); + return res; +} + static PyObject * do_warn(PyObject *message, PyObject *category, Py_ssize_t stack_level, PyObject *source) @@ -1077,6 +1380,53 @@ warnings_warn_explicit(PyObject *self, PyObject *args, PyObject *kwds) return returned; } +static PyObject * +warnings_warn_explicit_with_fix(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwd_list[] = {"message", "fix", "category", "filename", "lineno", + "module", "registry", "module_globals", + "source", 0}; + PyObject *message; + PyObject *fix; + PyObject *category; + PyObject *filename; + int lineno; + PyObject *module = NULL; + PyObject *registry = NULL; + PyObject *module_globals = NULL; + PyObject *sourceobj = NULL; + PyObject *source_line = NULL; + PyObject *returned; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOUi|OOOO:warn_explicit_with_fix", + kwd_list, &message, &fix, &category, &filename, &lineno, &module, + ®istry, &module_globals, &sourceobj)) + return NULL; + + PyThreadState *tstate = get_current_tstate(); + if (tstate == NULL) { + return NULL; + } + + if (module_globals && module_globals != Py_None) { + if (!PyDict_Check(module_globals)) { + PyErr_Format(PyExc_TypeError, + "module_globals must be a dict, not '%.200s'", + Py_TYPE(module_globals)->tp_name); + return NULL; + } + + source_line = get_source_line(tstate->interp, module_globals, lineno); + if (source_line == NULL && PyErr_Occurred()) { + return NULL; + } + } + returned = warn_explicit_with_fix(tstate, category, message, fix, filename, lineno, module, + registry, source_line, sourceobj); + Py_XDECREF(source_line); + return returned; +} + static PyObject * warnings_filters_mutated(PyObject *self, PyObject *Py_UNUSED(args)) { @@ -1112,6 +1462,23 @@ warn_unicode(PyObject *category, PyObject *message, return 0; } +static int +warn_unicode_with_fix(PyObject *category, PyObject *message, PyObject *fix, + Py_ssize_t stack_level, PyObject *source) +{ + PyObject *res; + + if (category == NULL) + category = PyExc_RuntimeWarning; + + res = do_warn_with_fix(message, fix, category, stack_level, source); + if (res == NULL) + return -1; + Py_DECREF(res); + + return 0; +} + static int _PyErr_WarnFormatV(PyObject *source, PyObject *category, Py_ssize_t stack_level, @@ -1194,6 +1561,20 @@ PyErr_WarnEx(PyObject *category, const char *text, Py_ssize_t stack_level) return ret; } +int +PyErr_WarnEx_WithFix(PyObject *category, const char *msg_txt, const char *fix_txt, Py_ssize_t stack_level) +{ + int ret; + PyObject *message = PyUnicode_FromString(msg_txt); + if (message == NULL) return -1; + PyObject *fix = PyUnicode_FromString(fix_txt); + if (fix == NULL) return -1; + ret = warn_unicode_with_fix(category, message, fix, stack_level, NULL); + Py_DECREF(message); + Py_DECREF(fix); + return ret; +} + /* PyErr_Warn is only for backwards compatibility and will be removed. Use PyErr_WarnEx instead. */ @@ -1226,6 +1607,26 @@ PyErr_WarnExplicitObject(PyObject *category, PyObject *message, return 0; } +int +PyErr_WarnExplicitWithFixObject(PyObject *category, PyObject *message, PyObject *fix, + PyObject *filename, int lineno, + PyObject *module, PyObject *registry) +{ + PyObject *res; + if (category == NULL) + category = PyExc_RuntimeWarning; + PyThreadState *tstate = get_current_tstate(); + if (tstate == NULL) { + return -1; + } + res = warn_explicit_with_fix(tstate, category, message, fix, filename, lineno, + module, registry, NULL, NULL); + if (res == NULL) + return -1; + Py_DECREF(res); + return 0; +} + int PyErr_WarnExplicit(PyObject *category, const char *text, const char *filename_str, int lineno, @@ -1254,6 +1655,36 @@ PyErr_WarnExplicit(PyObject *category, const char *text, return ret; } +int +PyErr_WarnExplicit_WithFix(PyObject *category, const char *msg_txt, const char *fix_txt, + const char *filename_str, int lineno, + const char *module_str, PyObject *registry) +{ + PyObject *message = PyUnicode_FromString(msg_txt); + PyObject *fix = PyUnicode_FromString(fix_txt); + PyObject *filename = PyUnicode_DecodeFSDefault(filename_str); + PyObject *module = NULL; + int ret = -1; + + if (message == NULL || filename == NULL || fix == NULL) + goto exit; + if (module_str != NULL) { + module = PyUnicode_FromString(module_str); + if (module == NULL) + goto exit; + } + + ret = PyErr_WarnExplicitWithFixObject(category, message, fix, filename, lineno, + module, registry); + + exit: + Py_XDECREF(message); + Py_XDECREF(fix); + Py_XDECREF(module); + Py_XDECREF(filename); + return ret; +} + int PyErr_WarnExplicitFormat(PyObject *category, const char *filename_str, int lineno, @@ -1351,10 +1782,15 @@ _PyErr_WarnUnawaitedCoroutine(PyObject *coro) PyDoc_STRVAR(warn_explicit_doc, "Low-level interface to warnings functionality."); +PyDoc_STRVAR(warn_explicit_with_fix_doc, +"Low-level interface to warnings functionality, with a fix argument."); + static PyMethodDef warnings_functions[] = { WARNINGS_WARN_METHODDEF {"warn_explicit", _PyCFunction_CAST(warnings_warn_explicit), METH_VARARGS | METH_KEYWORDS, warn_explicit_doc}, + {"warn_explicit_with_fix", _PyCFunction_CAST(warnings_warn_explicit_with_fix), + METH_VARARGS | METH_KEYWORDS, warn_explicit_with_fix_doc}, {"_filters_mutated", _PyCFunction_CAST(warnings_filters_mutated), METH_NOARGS, NULL}, /* XXX(brett.cannon): add showwarning? */ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 9bcfe03cbbd9f3..a1108e272e3043 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2853,6 +2853,7 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) SET_SYS("maxsize", PyLong_FromSsize_t(PY_SSIZE_T_MAX)); SET_SYS("float_info", PyFloat_GetInfo()); SET_SYS("int_info", PyLong_GetInfo()); + SET_SYS("py2x_warning", PyBool_FromLong(Py_Py2xWarningFlag)); /* initialize hash_info */ if (Hash_InfoType.tp_name == NULL) { if (PyStructSequence_InitType2(&Hash_InfoType, &hash_info_desc) < 0) { diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 63d0695bfb4a6e..c449d4b9c18fd3 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -391,6 +391,7 @@ Objects/weakrefobject.c weakref_call kwlist - Objects/exceptions.c NameError_init kwlist - Objects/exceptions.c AttributeError_init kwlist - Python/_warnings.c warnings_warn_explicit kwd_list - +Python/_warnings.c warnings_warn_explicit_with_fix kwd_list - Python/bltinmodule.c builtin___import__ kwlist - Python/bltinmodule.c min_max kwlist - Python/bltinmodule.c zip_new kwlist - diff --git a/Tools/scripts/generate_global_objects.py b/Tools/scripts/generate_global_objects.py index 2180acd7ea94bc..90de4e73af5624 100644 --- a/Tools/scripts/generate_global_objects.py +++ b/Tools/scripts/generate_global_objects.py @@ -22,7 +22,9 @@ # from GET_WARNINGS_ATTR() in Python/_warnings.c 'WarningMessage', + 'WarningMessageAndFix', '_showwarnmsg', + '_showwarnmsgwithfix', '_warn_unawaited_coroutine', 'defaultaction', 'filters',