Integrate warnings filtering directly into Config (#7700)
Warnings are a central part of Python, so much that Python itself has command-line and environtment variables to handle warnings. By moving the concept of warning handling into Config, it becomes natural to filter warnings issued as early as possible, even before the "_pytest.warnings" plugin is given a chance to spring into action. This also avoids the weird coupling between config and the warnings plugin that was required before. Fix #6681 Fix #2891 Fix #7620 Fix #7626 Close #7649 Co-authored-by: Ran Benita <ran@unusedvar.com>
This commit is contained in:
		
							parent
							
								
									91dbdb6093
								
							
						
					
					
						commit
						19e99ab413
					
				|  | @ -0,0 +1,3 @@ | |||
| Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``. | ||||
| 
 | ||||
| This also fixes a number of long standing issues: `#2891 <https://github.com/pytest-dev/pytest/issues/2891>`__, `#7620 <https://github.com/pytest-dev/pytest/issues/7620>`__, `#7426 <https://github.com/pytest-dev/pytest/issues/7426>`__. | ||||
|  | @ -267,13 +267,11 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) | |||
| 
 | ||||
|     def _warn_already_imported(self, name: str) -> None: | ||||
|         from _pytest.warning_types import PytestAssertRewriteWarning | ||||
|         from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|         _issue_warning_captured( | ||||
|         self.config.issue_config_time_warning( | ||||
|             PytestAssertRewriteWarning( | ||||
|                 "Module already imported so cannot be rewritten: %s" % name | ||||
|             ), | ||||
|             self.config.hook, | ||||
|             stacklevel=5, | ||||
|         ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import copy | |||
| import enum | ||||
| import inspect | ||||
| import os | ||||
| import re | ||||
| import shlex | ||||
| import sys | ||||
| import types | ||||
|  | @ -15,6 +16,7 @@ from types import TracebackType | |||
| from typing import Any | ||||
| from typing import Callable | ||||
| from typing import Dict | ||||
| from typing import Generator | ||||
| from typing import IO | ||||
| from typing import Iterable | ||||
| from typing import Iterator | ||||
|  | @ -342,6 +344,13 @@ class PytestPluginManager(PluginManager): | |||
|         self._noconftest = False | ||||
|         self._duplicatepaths = set()  # type: Set[py.path.local] | ||||
| 
 | ||||
|         # plugins that were explicitly skipped with pytest.skip | ||||
|         # list of (module name, skip reason) | ||||
|         # previously we would issue a warning when a plugin was skipped, but | ||||
|         # since we refactored warnings as first citizens of Config, they are | ||||
|         # just stored here to be used later. | ||||
|         self.skipped_plugins = []  # type: List[Tuple[str, str]] | ||||
| 
 | ||||
|         self.add_hookspecs(_pytest.hookspec) | ||||
|         self.register(self) | ||||
|         if os.environ.get("PYTEST_DEBUG"): | ||||
|  | @ -694,13 +703,7 @@ class PytestPluginManager(PluginManager): | |||
|             ).with_traceback(e.__traceback__) from e | ||||
| 
 | ||||
|         except Skipped as e: | ||||
|             from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|             _issue_warning_captured( | ||||
|                 PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)), | ||||
|                 self.hook, | ||||
|                 stacklevel=2, | ||||
|             ) | ||||
|             self.skipped_plugins.append((modname, e.msg or "")) | ||||
|         else: | ||||
|             mod = sys.modules[importspec] | ||||
|             self.register(mod, modname) | ||||
|  | @ -1092,6 +1095,9 @@ class Config: | |||
|                 self._validate_args(self.getini("addopts"), "via addopts config") + args | ||||
|             ) | ||||
| 
 | ||||
|         self.known_args_namespace = self._parser.parse_known_args( | ||||
|             args, namespace=copy.copy(self.option) | ||||
|         ) | ||||
|         self._checkversion() | ||||
|         self._consider_importhook(args) | ||||
|         self.pluginmanager.consider_preparse(args, exclude_only=False) | ||||
|  | @ -1100,10 +1106,10 @@ class Config: | |||
|             # plugins are going to be loaded. | ||||
|             self.pluginmanager.load_setuptools_entrypoints("pytest11") | ||||
|         self.pluginmanager.consider_env() | ||||
|         self.known_args_namespace = ns = self._parser.parse_known_args( | ||||
|             args, namespace=copy.copy(self.option) | ||||
|         ) | ||||
| 
 | ||||
|         self._validate_plugins() | ||||
|         self._warn_about_skipped_plugins() | ||||
| 
 | ||||
|         if self.known_args_namespace.confcutdir is None and self.inifile: | ||||
|             confcutdir = py.path.local(self.inifile).dirname | ||||
|             self.known_args_namespace.confcutdir = confcutdir | ||||
|  | @ -1112,21 +1118,24 @@ class Config: | |||
|                 early_config=self, args=args, parser=self._parser | ||||
|             ) | ||||
|         except ConftestImportFailure as e: | ||||
|             if ns.help or ns.version: | ||||
|             if self.known_args_namespace.help or self.known_args_namespace.version: | ||||
|                 # we don't want to prevent --help/--version to work | ||||
|                 # so just let is pass and print a warning at the end | ||||
|                 from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|                 _issue_warning_captured( | ||||
|                 self.issue_config_time_warning( | ||||
|                     PytestConfigWarning( | ||||
|                         "could not load initial conftests: {}".format(e.path) | ||||
|                     ), | ||||
|                     self.hook, | ||||
|                     stacklevel=2, | ||||
|                 ) | ||||
|             else: | ||||
|                 raise | ||||
|         self._validate_keys() | ||||
| 
 | ||||
|     @hookimpl(hookwrapper=True) | ||||
|     def pytest_collection(self) -> Generator[None, None, None]: | ||||
|         """Validate invalid ini keys after collection is done so we take in account | ||||
|         options added by late-loading conftest files.""" | ||||
|         yield | ||||
|         self._validate_config_options() | ||||
| 
 | ||||
|     def _checkversion(self) -> None: | ||||
|         import pytest | ||||
|  | @ -1147,9 +1156,9 @@ class Config: | |||
|                     % (self.inifile, minver, pytest.__version__,) | ||||
|                 ) | ||||
| 
 | ||||
|     def _validate_keys(self) -> None: | ||||
|     def _validate_config_options(self) -> None: | ||||
|         for key in sorted(self._get_unknown_ini_keys()): | ||||
|             self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key)) | ||||
|             self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key)) | ||||
| 
 | ||||
|     def _validate_plugins(self) -> None: | ||||
|         required_plugins = sorted(self.getini("required_plugins")) | ||||
|  | @ -1165,7 +1174,6 @@ class Config: | |||
| 
 | ||||
|         missing_plugins = [] | ||||
|         for required_plugin in required_plugins: | ||||
|             spec = None | ||||
|             try: | ||||
|                 spec = Requirement(required_plugin) | ||||
|             except InvalidRequirement: | ||||
|  | @ -1187,11 +1195,7 @@ class Config: | |||
|         if self.known_args_namespace.strict_config: | ||||
|             fail(message, pytrace=False) | ||||
| 
 | ||||
|         from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|         _issue_warning_captured( | ||||
|             PytestConfigWarning(message), self.hook, stacklevel=3, | ||||
|         ) | ||||
|         self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) | ||||
| 
 | ||||
|     def _get_unknown_ini_keys(self) -> List[str]: | ||||
|         parser_inicfg = self._parser._inidict | ||||
|  | @ -1222,6 +1226,49 @@ class Config: | |||
|         except PrintHelp: | ||||
|             pass | ||||
| 
 | ||||
|     def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: | ||||
|         """Issue and handle a warning during the "configure" stage. | ||||
| 
 | ||||
|         During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` | ||||
|         function because it is not possible to have hookwrappers around ``pytest_configure``. | ||||
| 
 | ||||
|         This function is mainly intended for plugins that need to issue warnings during | ||||
|         ``pytest_configure`` (or similar stages). | ||||
| 
 | ||||
|         :param warning: The warning instance. | ||||
|         :param stacklevel: stacklevel forwarded to warnings.warn. | ||||
|         """ | ||||
|         if self.pluginmanager.is_blocked("warnings"): | ||||
|             return | ||||
| 
 | ||||
|         cmdline_filters = self.known_args_namespace.pythonwarnings or [] | ||||
|         config_filters = self.getini("filterwarnings") | ||||
| 
 | ||||
|         with warnings.catch_warnings(record=True) as records: | ||||
|             warnings.simplefilter("always", type(warning)) | ||||
|             apply_warning_filters(config_filters, cmdline_filters) | ||||
|             warnings.warn(warning, stacklevel=stacklevel) | ||||
| 
 | ||||
|         if records: | ||||
|             frame = sys._getframe(stacklevel - 1) | ||||
|             location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name | ||||
|             self.hook.pytest_warning_captured.call_historic( | ||||
|                 kwargs=dict( | ||||
|                     warning_message=records[0], | ||||
|                     when="config", | ||||
|                     item=None, | ||||
|                     location=location, | ||||
|                 ) | ||||
|             ) | ||||
|             self.hook.pytest_warning_recorded.call_historic( | ||||
|                 kwargs=dict( | ||||
|                     warning_message=records[0], | ||||
|                     when="config", | ||||
|                     nodeid="", | ||||
|                     location=location, | ||||
|                 ) | ||||
|             ) | ||||
| 
 | ||||
|     def addinivalue_line(self, name: str, line: str) -> None: | ||||
|         """Add a line to an ini-file option. The option must have been | ||||
|         declared but might not yet be set in which case the line becomes | ||||
|  | @ -1365,8 +1412,6 @@ class Config: | |||
| 
 | ||||
|     def _warn_about_missing_assertion(self, mode: str) -> None: | ||||
|         if not _assertion_supported(): | ||||
|             from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|             if mode == "plain": | ||||
|                 warning_text = ( | ||||
|                     "ASSERTIONS ARE NOT EXECUTED" | ||||
|  | @ -1381,8 +1426,15 @@ class Config: | |||
|                     "by the underlying Python interpreter " | ||||
|                     "(are you using python -O?)\n" | ||||
|                 ) | ||||
|             _issue_warning_captured( | ||||
|                 PytestConfigWarning(warning_text), self.hook, stacklevel=3, | ||||
|             self.issue_config_time_warning( | ||||
|                 PytestConfigWarning(warning_text), stacklevel=3, | ||||
|             ) | ||||
| 
 | ||||
|     def _warn_about_skipped_plugins(self) -> None: | ||||
|         for module_name, msg in self.pluginmanager.skipped_plugins: | ||||
|             self.issue_config_time_warning( | ||||
|                 PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)), | ||||
|                 stacklevel=2, | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1435,3 +1487,51 @@ def _strtobool(val: str) -> bool: | |||
|         return False | ||||
|     else: | ||||
|         raise ValueError("invalid truth value {!r}".format(val)) | ||||
| 
 | ||||
| 
 | ||||
| @lru_cache(maxsize=50) | ||||
| def parse_warning_filter( | ||||
|     arg: str, *, escape: bool | ||||
| ) -> "Tuple[str, str, Type[Warning], str, int]": | ||||
|     """Parse a warnings filter string. | ||||
| 
 | ||||
|     This is copied from warnings._setoption, but does not apply the filter, | ||||
|     only parses it, and makes the escaping optional. | ||||
|     """ | ||||
|     parts = arg.split(":") | ||||
|     if len(parts) > 5: | ||||
|         raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) | ||||
|     while len(parts) < 5: | ||||
|         parts.append("") | ||||
|     action_, message, category_, module, lineno_ = [s.strip() for s in parts] | ||||
|     action = warnings._getaction(action_)  # type: str # type: ignore[attr-defined] | ||||
|     category = warnings._getcategory( | ||||
|         category_ | ||||
|     )  # type: Type[Warning] # type: ignore[attr-defined] | ||||
|     if message and escape: | ||||
|         message = re.escape(message) | ||||
|     if module and escape: | ||||
|         module = re.escape(module) + r"\Z" | ||||
|     if lineno_: | ||||
|         try: | ||||
|             lineno = int(lineno_) | ||||
|             if lineno < 0: | ||||
|                 raise ValueError | ||||
|         except (ValueError, OverflowError) as e: | ||||
|             raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e | ||||
|     else: | ||||
|         lineno = 0 | ||||
|     return action, message, category, module, lineno | ||||
| 
 | ||||
| 
 | ||||
| def apply_warning_filters( | ||||
|     config_filters: Iterable[str], cmdline_filters: Iterable[str] | ||||
| ) -> None: | ||||
|     """Applies pytest-configured filters to the warnings module""" | ||||
|     # Filters should have this precedence: cmdline options, config. | ||||
|     # Filters should be applied in the inverse order of precedence. | ||||
|     for arg in config_filters: | ||||
|         warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) | ||||
| 
 | ||||
|     for arg in cmdline_filters: | ||||
|         warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) | ||||
|  |  | |||
|  | @ -30,18 +30,15 @@ def pytest_configure(config: Config) -> None: | |||
|         # of enabling faulthandler before each test executes. | ||||
|         config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks") | ||||
|     else: | ||||
|         from _pytest.warnings import _issue_warning_captured | ||||
| 
 | ||||
|         # Do not handle dumping to stderr if faulthandler is already enabled, so warn | ||||
|         # users that the option is being ignored. | ||||
|         timeout = FaultHandlerHooks.get_timeout_config_value(config) | ||||
|         if timeout > 0: | ||||
|             _issue_warning_captured( | ||||
|             config.issue_config_time_warning( | ||||
|                 pytest.PytestConfigWarning( | ||||
|                     "faulthandler module enabled before pytest configuration step, " | ||||
|                     "'faulthandler_timeout' option ignored" | ||||
|                 ), | ||||
|                 config.hook, | ||||
|                 stacklevel=2, | ||||
|             ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -69,6 +69,20 @@ def pytest_addoption(parser: Parser) -> None: | |||
|         const=1, | ||||
|         help="exit instantly on first error or failed test.", | ||||
|     ) | ||||
|     group = parser.getgroup("pytest-warnings") | ||||
|     group.addoption( | ||||
|         "-W", | ||||
|         "--pythonwarnings", | ||||
|         action="append", | ||||
|         help="set which warnings to report, see -W option of python itself.", | ||||
|     ) | ||||
|     parser.addini( | ||||
|         "filterwarnings", | ||||
|         type="linelist", | ||||
|         help="Each line specifies a pattern for " | ||||
|         "warnings.filterwarnings. " | ||||
|         "Processed after -W/--pythonwarnings.", | ||||
|     ) | ||||
|     group._addoption( | ||||
|         "--maxfail", | ||||
|         metavar="num", | ||||
|  |  | |||
|  | @ -1,77 +1,22 @@ | |||
| import re | ||||
| import sys | ||||
| import warnings | ||||
| from contextlib import contextmanager | ||||
| from functools import lru_cache | ||||
| from typing import Generator | ||||
| from typing import Optional | ||||
| from typing import Tuple | ||||
| 
 | ||||
| import pytest | ||||
| from _pytest.compat import TYPE_CHECKING | ||||
| from _pytest.config import apply_warning_filters | ||||
| from _pytest.config import Config | ||||
| from _pytest.config.argparsing import Parser | ||||
| from _pytest.config import parse_warning_filter | ||||
| from _pytest.main import Session | ||||
| from _pytest.nodes import Item | ||||
| from _pytest.terminal import TerminalReporter | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from typing import Type | ||||
|     from typing_extensions import Literal | ||||
| 
 | ||||
| 
 | ||||
| @lru_cache(maxsize=50) | ||||
| def _parse_filter( | ||||
|     arg: str, *, escape: bool | ||||
| ) -> "Tuple[str, str, Type[Warning], str, int]": | ||||
|     """Parse a warnings filter string. | ||||
| 
 | ||||
|     This is copied from warnings._setoption, but does not apply the filter, | ||||
|     only parses it, and makes the escaping optional. | ||||
|     """ | ||||
|     parts = arg.split(":") | ||||
|     if len(parts) > 5: | ||||
|         raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) | ||||
|     while len(parts) < 5: | ||||
|         parts.append("") | ||||
|     action_, message, category_, module, lineno_ = [s.strip() for s in parts] | ||||
|     action = warnings._getaction(action_)  # type: str # type: ignore[attr-defined] | ||||
|     category = warnings._getcategory( | ||||
|         category_ | ||||
|     )  # type: Type[Warning] # type: ignore[attr-defined] | ||||
|     if message and escape: | ||||
|         message = re.escape(message) | ||||
|     if module and escape: | ||||
|         module = re.escape(module) + r"\Z" | ||||
|     if lineno_: | ||||
|         try: | ||||
|             lineno = int(lineno_) | ||||
|             if lineno < 0: | ||||
|                 raise ValueError | ||||
|         except (ValueError, OverflowError) as e: | ||||
|             raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e | ||||
|     else: | ||||
|         lineno = 0 | ||||
|     return (action, message, category, module, lineno) | ||||
| 
 | ||||
| 
 | ||||
| def pytest_addoption(parser: Parser) -> None: | ||||
|     group = parser.getgroup("pytest-warnings") | ||||
|     group.addoption( | ||||
|         "-W", | ||||
|         "--pythonwarnings", | ||||
|         action="append", | ||||
|         help="set which warnings to report, see -W option of python itself.", | ||||
|     ) | ||||
|     parser.addini( | ||||
|         "filterwarnings", | ||||
|         type="linelist", | ||||
|         help="Each line specifies a pattern for " | ||||
|         "warnings.filterwarnings. " | ||||
|         "Processed after -W/--pythonwarnings.", | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def pytest_configure(config: Config) -> None: | ||||
|     config.addinivalue_line( | ||||
|         "markers", | ||||
|  | @ -93,8 +38,8 @@ def catch_warnings_for_item( | |||
| 
 | ||||
|     Each warning captured triggers the ``pytest_warning_recorded`` hook. | ||||
|     """ | ||||
|     cmdline_filters = config.getoption("pythonwarnings") or [] | ||||
|     inifilters = config.getini("filterwarnings") | ||||
|     config_filters = config.getini("filterwarnings") | ||||
|     cmdline_filters = config.known_args_namespace.pythonwarnings or [] | ||||
|     with warnings.catch_warnings(record=True) as log: | ||||
|         # mypy can't infer that record=True means log is not None; help it. | ||||
|         assert log is not None | ||||
|  | @ -104,19 +49,14 @@ def catch_warnings_for_item( | |||
|             warnings.filterwarnings("always", category=DeprecationWarning) | ||||
|             warnings.filterwarnings("always", category=PendingDeprecationWarning) | ||||
| 
 | ||||
|         # Filters should have this precedence: mark, cmdline options, ini. | ||||
|         # Filters should be applied in the inverse order of precedence. | ||||
|         for arg in inifilters: | ||||
|             warnings.filterwarnings(*_parse_filter(arg, escape=False)) | ||||
| 
 | ||||
|         for arg in cmdline_filters: | ||||
|             warnings.filterwarnings(*_parse_filter(arg, escape=True)) | ||||
|         apply_warning_filters(config_filters, cmdline_filters) | ||||
| 
 | ||||
|         # apply filters from "filterwarnings" marks | ||||
|         nodeid = "" if item is None else item.nodeid | ||||
|         if item is not None: | ||||
|             for mark in item.iter_markers(name="filterwarnings"): | ||||
|                 for arg in mark.args: | ||||
|                     warnings.filterwarnings(*_parse_filter(arg, escape=False)) | ||||
|                     warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) | ||||
| 
 | ||||
|         yield | ||||
| 
 | ||||
|  | @ -189,30 +129,11 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: | |||
|         yield | ||||
| 
 | ||||
| 
 | ||||
| def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None: | ||||
|     """A function that should be used instead of calling ``warnings.warn`` | ||||
|     directly when we are in the "configure" stage. | ||||
| 
 | ||||
|     At this point the actual options might not have been set, so we manually | ||||
|     trigger the pytest_warning_recorded hook so we can display these warnings | ||||
|     in the terminal. This is a hack until we can sort out #2891. | ||||
| 
 | ||||
|     :param warning: The warning instance. | ||||
|     :param hook: The hook caller. | ||||
|     :param stacklevel: stacklevel forwarded to warnings.warn. | ||||
|     """ | ||||
|     with warnings.catch_warnings(record=True) as records: | ||||
|         warnings.simplefilter("always", type(warning)) | ||||
|         warnings.warn(warning, stacklevel=stacklevel) | ||||
|     frame = sys._getframe(stacklevel - 1) | ||||
|     location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name | ||||
|     hook.pytest_warning_captured.call_historic( | ||||
|         kwargs=dict( | ||||
|             warning_message=records[0], when="config", item=None, location=location | ||||
|         ) | ||||
|     ) | ||||
|     hook.pytest_warning_recorded.call_historic( | ||||
|         kwargs=dict( | ||||
|             warning_message=records[0], when="config", nodeid="", location=location | ||||
|         ) | ||||
|     ) | ||||
| @pytest.hookimpl(hookwrapper=True) | ||||
| def pytest_load_initial_conftests( | ||||
|     early_config: "Config", | ||||
| ) -> Generator[None, None, None]: | ||||
|     with catch_warnings_for_item( | ||||
|         config=early_config, ihook=early_config.hook, when="config", item=None | ||||
|     ): | ||||
|         yield | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import textwrap | |||
| from typing import Dict | ||||
| from typing import List | ||||
| from typing import Sequence | ||||
| from typing import Tuple | ||||
| 
 | ||||
| import attr | ||||
| import py.path | ||||
|  | @ -12,11 +13,14 @@ import py.path | |||
| import _pytest._code | ||||
| import pytest | ||||
| from _pytest.compat import importlib_metadata | ||||
| from _pytest.compat import TYPE_CHECKING | ||||
| from _pytest.config import _get_plugin_specs_as_list | ||||
| from _pytest.config import _iter_rewritable_modules | ||||
| from _pytest.config import _strtobool | ||||
| from _pytest.config import Config | ||||
| from _pytest.config import ConftestImportFailure | ||||
| from _pytest.config import ExitCode | ||||
| from _pytest.config import parse_warning_filter | ||||
| from _pytest.config.exceptions import UsageError | ||||
| from _pytest.config.findpaths import determine_setup | ||||
| from _pytest.config.findpaths import get_common_ancestor | ||||
|  | @ -25,6 +29,9 @@ from _pytest.monkeypatch import MonkeyPatch | |||
| from _pytest.pathlib import Path | ||||
| from _pytest.pytester import Testdir | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from typing import Type | ||||
| 
 | ||||
| 
 | ||||
| class TestParseIni: | ||||
|     @pytest.mark.parametrize( | ||||
|  | @ -183,10 +190,10 @@ class TestParseIni: | |||
|                 ["unknown_ini", "another_unknown_ini"], | ||||
|                 [ | ||||
|                     "=*= warnings summary =*=", | ||||
|                     "*PytestConfigWarning:*Unknown config ini key: another_unknown_ini", | ||||
|                     "*PytestConfigWarning:*Unknown config ini key: unknown_ini", | ||||
|                     "*PytestConfigWarning:*Unknown config option: another_unknown_ini", | ||||
|                     "*PytestConfigWarning:*Unknown config option: unknown_ini", | ||||
|                 ], | ||||
|                 "Unknown config ini key: another_unknown_ini", | ||||
|                 "Unknown config option: another_unknown_ini", | ||||
|             ), | ||||
|             ( | ||||
|                 """ | ||||
|  | @ -197,9 +204,9 @@ class TestParseIni: | |||
|                 ["unknown_ini"], | ||||
|                 [ | ||||
|                     "=*= warnings summary =*=", | ||||
|                     "*PytestConfigWarning:*Unknown config ini key: unknown_ini", | ||||
|                     "*PytestConfigWarning:*Unknown config option: unknown_ini", | ||||
|                 ], | ||||
|                 "Unknown config ini key: unknown_ini", | ||||
|                 "Unknown config option: unknown_ini", | ||||
|             ), | ||||
|             ( | ||||
|                 """ | ||||
|  | @ -232,7 +239,8 @@ class TestParseIni: | |||
|             ), | ||||
|         ], | ||||
|     ) | ||||
|     def test_invalid_ini_keys( | ||||
|     @pytest.mark.filterwarnings("default") | ||||
|     def test_invalid_config_options( | ||||
|         self, testdir, ini_file_text, invalid_keys, warning_output, exception_text | ||||
|     ): | ||||
|         testdir.makeconftest( | ||||
|  | @ -250,10 +258,40 @@ class TestParseIni: | |||
|         result.stdout.fnmatch_lines(warning_output) | ||||
| 
 | ||||
|         if exception_text: | ||||
|             with pytest.raises(pytest.fail.Exception, match=exception_text): | ||||
|                 testdir.runpytest("--strict-config") | ||||
|         else: | ||||
|             testdir.runpytest("--strict-config") | ||||
|             result = testdir.runpytest("--strict-config") | ||||
|             result.stdout.fnmatch_lines("INTERNALERROR>*" + exception_text) | ||||
| 
 | ||||
|     @pytest.mark.filterwarnings("default") | ||||
|     def test_silence_unknown_key_warning(self, testdir: Testdir) -> None: | ||||
|         """Unknown config key warnings can be silenced using filterwarnings (#7620)""" | ||||
|         testdir.makeini( | ||||
|             """ | ||||
|             [pytest] | ||||
|             filterwarnings = | ||||
|                 ignore:Unknown config option:pytest.PytestConfigWarning | ||||
|             foobar=1 | ||||
|         """ | ||||
|         ) | ||||
|         result = testdir.runpytest() | ||||
|         result.stdout.no_fnmatch_line("*PytestConfigWarning*") | ||||
| 
 | ||||
|     @pytest.mark.filterwarnings("default") | ||||
|     def test_disable_warnings_plugin_disables_config_warnings( | ||||
|         self, testdir: Testdir | ||||
|     ) -> None: | ||||
|         """Disabling 'warnings' plugin also disables config time warnings""" | ||||
|         testdir.makeconftest( | ||||
|             """ | ||||
|             import pytest | ||||
|             def pytest_configure(config): | ||||
|                 config.issue_config_time_warning( | ||||
|                     pytest.PytestConfigWarning("custom config warning"), | ||||
|                     stacklevel=2, | ||||
|                 ) | ||||
|         """ | ||||
|         ) | ||||
|         result = testdir.runpytest("-pno:warnings") | ||||
|         result.stdout.no_fnmatch_line("*PytestConfigWarning*") | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "ini_file_text, exception_text", | ||||
|  | @ -1132,7 +1170,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test): | |||
|     pm.register(m) | ||||
|     hc = pm.hook.pytest_load_initial_conftests | ||||
|     values = hc._nonwrappers + hc._wrappers | ||||
|     expected = ["_pytest.config", m.__module__, "_pytest.capture"] | ||||
|     expected = ["_pytest.config", m.__module__, "_pytest.capture", "_pytest.warnings"] | ||||
|     assert [x.function.__module__ for x in values] == expected | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1816,3 +1854,52 @@ def test_conftest_import_error_repr(tmpdir): | |||
|             assert exc.__traceback__ is not None | ||||
|             exc_info = (type(exc), exc, exc.__traceback__) | ||||
|             raise ConftestImportFailure(path, exc_info) from exc | ||||
| 
 | ||||
| 
 | ||||
| def test_strtobool(): | ||||
|     assert _strtobool("YES") | ||||
|     assert not _strtobool("NO") | ||||
|     with pytest.raises(ValueError): | ||||
|         _strtobool("unknown") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "arg, escape, expected", | ||||
|     [ | ||||
|         ("ignore", False, ("ignore", "", Warning, "", 0)), | ||||
|         ( | ||||
|             "ignore::DeprecationWarning", | ||||
|             False, | ||||
|             ("ignore", "", DeprecationWarning, "", 0), | ||||
|         ), | ||||
|         ( | ||||
|             "ignore:some msg:DeprecationWarning", | ||||
|             False, | ||||
|             ("ignore", "some msg", DeprecationWarning, "", 0), | ||||
|         ), | ||||
|         ( | ||||
|             "ignore::DeprecationWarning:mod", | ||||
|             False, | ||||
|             ("ignore", "", DeprecationWarning, "mod", 0), | ||||
|         ), | ||||
|         ( | ||||
|             "ignore::DeprecationWarning:mod:42", | ||||
|             False, | ||||
|             ("ignore", "", DeprecationWarning, "mod", 42), | ||||
|         ), | ||||
|         ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)), | ||||
|         ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)), | ||||
|     ], | ||||
| ) | ||||
| def test_parse_warning_filter( | ||||
|     arg: str, escape: bool, expected: "Tuple[str, str, Type[Warning], str, int]" | ||||
| ) -> None: | ||||
|     assert parse_warning_filter(arg, escape=escape) == expected | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"]) | ||||
| def test_parse_warning_filter_failure(arg: str) -> None: | ||||
|     import warnings | ||||
| 
 | ||||
|     with pytest.raises(warnings._OptionError): | ||||
|         parse_warning_filter(arg, escape=True) | ||||
|  |  | |||
|  | @ -240,9 +240,8 @@ def test_filterwarnings_mark_registration(testdir): | |||
| def test_warning_captured_hook(testdir): | ||||
|     testdir.makeconftest( | ||||
|         """ | ||||
|         from _pytest.warnings import _issue_warning_captured | ||||
|         def pytest_configure(config): | ||||
|             _issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2) | ||||
|             config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2) | ||||
|     """ | ||||
|     ) | ||||
|     testdir.makepyfile( | ||||
|  | @ -716,10 +715,22 @@ class TestStackLevel: | |||
|         assert "config{sep}__init__.py".format(sep=os.sep) in file | ||||
|         assert func == "_preparse" | ||||
| 
 | ||||
|     @pytest.mark.filterwarnings("default") | ||||
|     def test_conftest_warning_captured(self, testdir: Testdir) -> None: | ||||
|         """Warnings raised during importing of conftest.py files is captured (#2891).""" | ||||
|         testdir.makeconftest( | ||||
|             """ | ||||
|             import warnings | ||||
|             warnings.warn(UserWarning("my custom warning")) | ||||
|             """ | ||||
|         ) | ||||
|         result = testdir.runpytest() | ||||
|         result.stdout.fnmatch_lines( | ||||
|             ["conftest.py:2", "*UserWarning: my custom warning*"] | ||||
|         ) | ||||
| 
 | ||||
|     def test_issue4445_import_plugin(self, testdir, capwarn): | ||||
|         """#4445: Make sure the warning points to a reasonable location | ||||
|         See origin of _issue_warning_captured at: _pytest.config.__init__.py:585 | ||||
|         """ | ||||
|         """#4445: Make sure the warning points to a reasonable location""" | ||||
|         testdir.makepyfile( | ||||
|             some_plugin=""" | ||||
|             import pytest | ||||
|  | @ -738,7 +749,7 @@ class TestStackLevel: | |||
| 
 | ||||
|         assert "skipped plugin 'some_plugin': thing" in str(warning.message) | ||||
|         assert "config{sep}__init__.py".format(sep=os.sep) in file | ||||
|         assert func == "import_plugin" | ||||
|         assert func == "_warn_about_skipped_plugins" | ||||
| 
 | ||||
|     def test_issue4445_issue5928_mark_generator(self, testdir): | ||||
|         """#4445 and #5928: Make sure the warning from an unknown mark points to | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue