From 0fffa6ba2f5458a22778551db7bf64b1fbd4f5b3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 19:27:46 -0300 Subject: [PATCH] Implement hack to issue warnings during config Once we can capture warnings during the config stage, we can then get rid of this function Related to #2891 --- doc/en/reference.rst | 2 ++ src/_pytest/config/__init__.py | 10 +++++---- src/_pytest/config/findpaths.py | 39 +++++++++++++++++---------------- src/_pytest/hookspec.py | 16 ++++++++++++-- src/_pytest/resultlog.py | 4 ++-- src/_pytest/warnings.py | 17 ++++++++++++++ testing/deprecated_test.py | 18 +++------------ 7 files changed, 64 insertions(+), 42 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index e19b5ae876..52d83cf6ee 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -611,6 +611,8 @@ Session related reporting hooks: .. autofunction:: pytest_terminal_summary .. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_post_finalizer +.. autofunction:: pytest_logwarning +.. autofunction:: pytest_warning_captured And here is the central hook for reporting about test execution: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index cec56e8008..e1f126af00 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,7 @@ def get_plugin_manager(): def _prepareconfig(args=None, plugins=None): - warning = None + warning_msg = None if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): @@ -165,7 +165,7 @@ def _prepareconfig(args=None, plugins=None): args = shlex.split(args, posix=sys.platform != "win32") from _pytest import deprecated - warning = deprecated.MAIN_STR_ARGS + warning_msg = deprecated.MAIN_STR_ARGS config = get_config() pluginmanager = config.pluginmanager try: @@ -175,10 +175,11 @@ def _prepareconfig(args=None, plugins=None): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) - if warning: + if warning_msg: from _pytest.warning_types import PytestWarning + from _pytest.warnings import _issue_config_warning - warnings.warn(warning, PytestWarning) + _issue_config_warning(PytestWarning(warning_msg), config=config) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) @@ -696,6 +697,7 @@ def _initini(self, args): ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, + config=self, ) self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info["rootdir"] = self.rootdir diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e10c455b1a..7480603bec 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -10,15 +10,12 @@ def exists(path, ignore=EnvironmentError): return False -def getcfg(args): +def getcfg(args, config=None): """ Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). - note: warnfunc is an optional function used to warn - about ini-files that use deprecated features. - This parameter should be removed when pytest - adopts standard deprecation warnings (#1804). + note: config is optional and used only to issue warnings explicitly (#2891). """ from _pytest.deprecated import CFG_PYTEST_SECTION @@ -34,13 +31,15 @@ def getcfg(args): if exists(p): iniconfig = py.iniconfig.IniConfig(p) if "pytest" in iniconfig.sections: - if inibasename == "setup.cfg": - import warnings + if inibasename == "setup.cfg" and config is not None: + from _pytest.warnings import _issue_config_warning from _pytest.warning_types import RemovedInPytest4Warning - warnings.warn( - CFG_PYTEST_SECTION.format(filename=inibasename), - RemovedInPytest4Warning, + _issue_config_warning( + RemovedInPytest4Warning( + CFG_PYTEST_SECTION.format(filename=inibasename) + ), + config=config, ) return base, p, iniconfig["pytest"] if ( @@ -99,7 +98,7 @@ def get_dir_from_path(path): return [get_dir_from_path(path) for path in possible_paths if path.exists()] -def determine_setup(inifile, args, rootdir_cmd_arg=None): +def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): dirs = get_dirs_from_args(args) if inifile: iniconfig = py.iniconfig.IniConfig(inifile) @@ -109,14 +108,16 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None): for section in sections: try: inicfg = iniconfig[section] - if is_cfg_file and section == "pytest": - from _pytest.warning_types import RemovedInPytest4Warning + if is_cfg_file and section == "pytest" and config is not None: from _pytest.deprecated import CFG_PYTEST_SECTION - import warnings + from _pytest.warning_types import RemovedInPytest4Warning + from _pytest.warnings import _issue_config_warning - warnings.warn( - CFG_PYTEST_SECTION.format(filename=str(inifile)), - RemovedInPytest4Warning, + _issue_config_warning( + RemovedInPytest4Warning( + CFG_PYTEST_SECTION.format(filename=str(inifile)) + ), + config, ) break except KeyError: @@ -124,13 +125,13 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None): rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor]) + rootdir, inifile, inicfg = getcfg([ancestor], config=config) if rootdir is None: for rootdir in ancestor.parts(reverse=True): if rootdir.join("setup.py").exists(): break else: - rootdir, inifile, inicfg = getcfg(dirs) + rootdir, inifile, inicfg = getcfg(dirs, config=config) if rootdir is None: rootdir = get_common_ancestor([py.path.local(), ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 001f59b86b..dac36b3067 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -526,7 +526,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus): @hookspec(historic=True) def pytest_logwarning(message, code, nodeid, fslocation): - """ process a warning specified by a message, a code string, + """ + .. deprecated:: 3.8 + + This hook is will stop working in a future release. + + pytest no longer triggers this hook, but the + terminal writer still implements it to display warnings issued by + :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be + an error in future releases. + + process a warning specified by a message, a code string, a nodeid and fslocation (both of which may be None if the warning is not tied to a particular node/location). @@ -538,7 +548,7 @@ def pytest_logwarning(message, code, nodeid, fslocation): @hookspec(historic=True) def pytest_warning_captured(warning_message, when, item): """ - Process a warning captured by the internal pytest plugin. + Process a warning captured by the internal pytest warnings plugin. :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains @@ -546,6 +556,8 @@ def pytest_warning_captured(warning_message, when, item): :param str when: Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. * ``"collect"``: during test collection. * ``"runtest"``: during test execution. diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 308abd251a..8a972eed73 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -31,10 +31,10 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG - import warnings from _pytest.warning_types import RemovedInPytest4Warning + from _pytest.warnings import _issue_config_warning - warnings.warn(RESULT_LOG, RemovedInPytest4Warning) + _issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config) def pytest_unconfigure(config): diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 952c4a0be1..986343fd31 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -146,3 +146,20 @@ def pytest_terminal_summary(terminalreporter): config = terminalreporter.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): yield + + +def _issue_config_warning(warning, config): + """ + This function 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_captured + hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + + :param warning: the warning instance. + :param config: + """ + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + warnings.warn(warning, stacklevel=2) + config.hook.pytest_warning_captured.call_historic( + kwargs=dict(warning_message=records[0], when="config", item=None) + ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 70c6df63f3..ec53bf7eb8 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -54,9 +54,6 @@ def test_funcarg_prefix(value): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_pytest_setup_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -72,9 +69,6 @@ def test_pytest_setup_cfg_deprecated(testdir): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_pytest_custom_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -89,18 +83,15 @@ def test_pytest_custom_cfg_deprecated(testdir): ) -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) -def test_str_args_deprecated(tmpdir, testdir): +def test_str_args_deprecated(tmpdir): """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" from _pytest.main import EXIT_NOTESTSCOLLECTED warnings = [] class Collect(object): - def pytest_logwarning(self, message): - warnings.append(message) + def pytest_warning_captured(self, warning_message): + warnings.append(str(warning_message.message)) ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) msg = ( @@ -116,9 +107,6 @@ def test_getfuncargvalue_is_deprecated(request): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"])