Remove config.warn, Node.warn; pytest_logwarning issues a warning when implemented

Fix #3078
This commit is contained in:
Bruno Oliveira 2018-12-11 20:02:36 -02:00
parent 26d202a7bd
commit fd48cd57f9
20 changed files with 109 additions and 271 deletions

View File

@ -0,0 +1,3 @@
Remove legacy internal warnings system: ``config.warn``, ``Node.warn``. The ``pytest_logwarning`` now issues a warning when implemented.
See our `docs <https://docs.pytest.org/en/latest/deprecations.html#config-warn-and-node-warn>`__ on information on how to update your code.

View File

@ -74,34 +74,6 @@ Becomes:
``Config.warn`` and ``Node.warn``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 3.8
Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning
system for its own warnings, so those two functions are now deprecated.
``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example:
.. code-block:: python
config.warn("C1", "some warning")
Becomes:
.. code-block:: python
warnings.warn(pytest.PytestWarning("some warning"))
``Node.warn`` now supports two signatures:
* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function.
The warning instance must be a PytestWarning or subclass.
* ``node.warn("CI", "some message")``: this code/message form is now **deprecated** and should be converted to the warning instance form above.
Calling fixtures directly
~~~~~~~~~~~~~~~~~~~~~~~~~
@ -350,7 +322,33 @@ This should be updated to make use of standard fixture mechanisms:
You can consult `funcarg comparison section in the docs <https://docs.pytest.org/en/latest/funcarg_compare.html>`_ for
more information.
This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings.
``Config.warn`` and ``Node.warn``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Removed in version 4.0.*
Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning
system for its own warnings, so those two functions are now deprecated.
``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example:
.. code-block:: python
config.warn("C1", "some warning")
Becomes:
.. code-block:: python
warnings.warn(pytest.PytestWarning("some warning"))
``Node.warn`` now supports two signatures:
* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function.
The warning instance must be a PytestWarning or subclass.
* ``node.warn("CI", "some message")``: this code/message form has been **removed** and should be converted to the warning instance form above.
``yield`` tests
~~~~~~~~~~~~~~~

View File

@ -618,7 +618,6 @@ 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

View File

@ -278,11 +278,11 @@ class AssertionRewritingHook(object):
def _warn_already_imported(self, name):
from _pytest.warning_types import PytestWarning
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
_issue_config_warning(
_issue_warning_captured(
PytestWarning("Module already imported so cannot be rewritten: %s" % name),
self.config,
self.config.hook,
stacklevel=5,
)

View File

@ -59,12 +59,12 @@ class Cache(object):
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
def warn(self, fmt, **args):
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
from _pytest.warning_types import PytestWarning
_issue_config_warning(
_issue_warning_captured(
PytestWarning(fmt.format(**args) if args else fmt),
self._config,
self._config.hook,
stacklevel=3,
)

View File

@ -26,11 +26,13 @@ from .exceptions import PrintHelp
from .exceptions import UsageError
from .findpaths import determine_setup
from .findpaths import exists
from _pytest import deprecated
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest.compat import lru_cache
from _pytest.compat import safe_str
from _pytest.outcomes import Skipped
from _pytest.warning_types import PytestWarning
hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")
@ -189,9 +191,9 @@ def _prepareconfig(args=None, plugins=None):
else:
pluginmanager.register(plugin)
if warning:
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
_issue_config_warning(warning, config=config, stacklevel=4)
_issue_warning_captured(warning, hook=config.hook, stacklevel=4)
return pluginmanager.hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args
)
@ -245,14 +247,7 @@ class PytestPluginManager(PluginManager):
Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>`
instead.
"""
warning = dict(
code="I2",
fslocation=_pytest._code.getfslineno(sys._getframe(1)),
nodeid=None,
message="use pluginmanager.add_hookspecs instead of "
"deprecated addhooks() method.",
)
self._warn(warning)
warnings.warn(deprecated.PLUGIN_MANAGER_ADDHOOKS, stacklevel=2)
return self.add_hookspecs(module_or_class)
def parse_hookimpl_opts(self, plugin, name):
@ -296,10 +291,12 @@ class PytestPluginManager(PluginManager):
def register(self, plugin, name=None):
if name in ["pytest_catchlog", "pytest_capturelog"]:
self._warn(
"{} plugin has been merged into the core, "
"please remove it from your requirements.".format(
name.replace("_", "-")
warnings.warn(
PytestWarning(
"{} plugin has been merged into the core, "
"please remove it from your requirements.".format(
name.replace("_", "-")
)
)
)
return
@ -336,14 +333,6 @@ class PytestPluginManager(PluginManager):
)
self._configured = True
def _warn(self, message):
kwargs = (
message
if isinstance(message, dict)
else {"code": "I1", "message": message, "fslocation": None, "nodeid": None}
)
self.hook.pytest_logwarning.call_historic(kwargs=kwargs)
#
# internal API for local conftest plugin handling
#
@ -542,7 +531,13 @@ class PytestPluginManager(PluginManager):
six.reraise(new_exc_type, new_exc, sys.exc_info()[2])
except Skipped as e:
self._warn("skipped plugin %r: %s" % ((modname, e.msg)))
from _pytest.warnings import _issue_warning_captured
_issue_warning_captured(
PytestWarning("skipped plugin %r: %s" % (modname, e.msg)),
self.hook,
stacklevel=1,
)
else:
mod = sys.modules[importspec]
self.register(mod, modname)
@ -617,7 +612,6 @@ class Config(object):
self._override_ini = ()
self._opt2dest = {}
self._cleanup = []
self._warn = self.pluginmanager._warn
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
self.invocation_dir = py.path.local()
@ -642,36 +636,6 @@ class Config(object):
fin = self._cleanup.pop()
fin()
def warn(self, code, message, fslocation=None, nodeid=None):
"""
.. deprecated:: 3.8
Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead.
Generate a warning for this test session.
"""
from _pytest.warning_types import RemovedInPytest4Warning
if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2:
filename, lineno = fslocation[:2]
else:
filename = "unknown file"
lineno = 0
msg = "config.warn has been deprecated, use warnings.warn instead"
if nodeid:
msg = "{}: {}".format(nodeid, msg)
warnings.warn_explicit(
RemovedInPytest4Warning(msg),
category=None,
filename=filename,
lineno=lineno,
)
self.hook.pytest_logwarning.call_historic(
kwargs=dict(
code=code, message=message, fslocation=fslocation, nodeid=nodeid
)
)
def get_terminal_writer(self):
return self.pluginmanager.get_plugin("terminalreporter")._tw
@ -826,7 +790,15 @@ class Config(object):
if ns.help or ns.version:
# we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end
self._warn("could not load initial conftests (%s)\n" % e.path)
from _pytest.warnings import _issue_warning_captured
_issue_warning_captured(
PytestWarning(
"could not load initial conftests: {}".format(e.path)
),
self.hook,
stacklevel=2,
)
else:
raise

View File

@ -34,14 +34,14 @@ def getcfg(args, config=None):
iniconfig = py.iniconfig.IniConfig(p)
if "pytest" in iniconfig.sections:
if inibasename == "setup.cfg" and config is not None:
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
from _pytest.warning_types import RemovedInPytest4Warning
_issue_config_warning(
_issue_warning_captured(
RemovedInPytest4Warning(
CFG_PYTEST_SECTION.format(filename=inibasename)
),
config=config,
hook=config.hook,
stacklevel=2,
)
return base, p, iniconfig["pytest"]
@ -112,13 +112,13 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
inicfg = iniconfig[section]
if is_cfg_file and section == "pytest" and config is not None:
from _pytest.deprecated import CFG_PYTEST_SECTION
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
# TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once
# the deprecation expires.
_issue_config_warning(
_issue_warning_captured(
CFG_PYTEST_SECTION.format(filename=str(inifile)),
config,
config.hook,
stacklevel=2,
)
break

View File

@ -75,10 +75,6 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
)
NODE_WARN = RemovedInPytest4Warning(
"Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead."
)
RAISES_EXEC = PytestDeprecationWarning(
"raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n"
"See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec"
@ -94,6 +90,13 @@ RECORD_XML_PROPERTY = RemovedInPytest4Warning(
'"record_xml_property" is now deprecated.'
)
PLUGIN_MANAGER_ADDHOOKS = PytestDeprecationWarning(
"use pluginmanager.add_hookspecs instead of deprecated addhooks() method."
)
COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
"pycollector makeitem was removed as it is an accidentially leaked internal api"
)
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning(
"Defining pytest_plugins in a non-top-level conftest is deprecated, "
@ -110,3 +113,8 @@ PYTEST_ENSURETEMP = RemovedInPytest4Warning(
"pytest/tmpdir_factory.ensuretemp is deprecated, \n"
"please use the tmp_path fixture or tmp_path_factory.mktemp"
)
PYTEST_LOGWARNING = PytestDeprecationWarning(
"pytest_logwarning is deprecated, no longer being called, and will be removed soon\n"
"please use pytest_warning_captured instead"
)

View File

@ -1,6 +1,8 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
from pluggy import HookspecMarker
from _pytest.deprecated import PYTEST_LOGWARNING
hookspec = HookspecMarker("pytest")
@ -496,7 +498,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
"""
@hookspec(historic=True)
@hookspec(historic=True, warn_on_impl=PYTEST_LOGWARNING)
def pytest_logwarning(message, code, nodeid, fslocation):
"""
.. deprecated:: 3.8

View File

@ -105,74 +105,7 @@ class Node(object):
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, getattr(self, "name", None))
def warn(self, _code_or_warning=None, message=None, code=None):
"""Issue a warning for this item.
Warnings will be displayed after the test session, unless explicitly suppressed.
This can be called in two forms:
**Warning instance**
This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings.
.. code-block:: python
node.warn(PytestWarning("some message"))
The warning instance must be a subclass of :class:`pytest.PytestWarning`.
**code/message (deprecated)**
This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another
warning about the deprecation:
.. code-block:: python
node.warn("CI", "some message")
:param Union[Warning,str] _code_or_warning:
warning instance or warning code (legacy). This parameter receives an underscore for backward
compatibility with the legacy code/message form, and will be replaced for something
more usual when the legacy form is removed.
:param Union[str,None] message: message to display when called in the legacy form.
:param str code: code for the warning, in legacy form when using keyword arguments.
:return:
"""
if message is None:
if _code_or_warning is None:
raise ValueError("code_or_warning must be given")
self._std_warn(_code_or_warning)
else:
if _code_or_warning and code:
raise ValueError(
"code_or_warning and code cannot both be passed to this function"
)
code = _code_or_warning or code
self._legacy_warn(code, message)
def _legacy_warn(self, code, message):
"""
.. deprecated:: 3.8
Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead.
Generate a warning with the given code and message for this item.
"""
from _pytest.deprecated import NODE_WARN
self._std_warn(NODE_WARN)
assert isinstance(code, str)
fslocation = get_fslocation_from_item(self)
self.ihook.pytest_logwarning.call_historic(
kwargs=dict(
code=code, message=message, nodeid=self.nodeid, fslocation=fslocation
)
)
def _std_warn(self, warning):
def warn(self, warning):
"""Issue a warning for this item.
Warnings will be displayed after the test session, unless explicitly suppressed
@ -180,6 +113,12 @@ class Node(object):
:param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
Example usage::
.. code-block:: python
node.warn(PytestWarning("some message"))
"""
from _pytest.warning_types import PytestWarning

View File

@ -34,9 +34,9 @@ def pytest_configure(config):
config.pluginmanager.register(config._resultlog)
from _pytest.deprecated import RESULT_LOG
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
_issue_config_warning(RESULT_LOG, config, stacklevel=2)
_issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2)
def pytest_unconfigure(config):

View File

@ -186,20 +186,17 @@ def pytest_report_teststatus(report):
@attr.s
class WarningReport(object):
"""
Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``.
Simple structure to hold warnings information captured by ``pytest_warning_captured``.
:ivar str message: user friendly message about the warning
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
:ivar tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
:ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook.
"""
message = attr.ib()
nodeid = attr.ib(default=None)
fslocation = attr.ib(default=None)
legacy = attr.ib(default=False)
def get_location(self, config):
"""
@ -329,13 +326,6 @@ class TerminalReporter(object):
self.write_line("INTERNALERROR> " + line)
return 1
def pytest_logwarning(self, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", [])
warning = WarningReport(
fslocation=fslocation, message=message, nodeid=nodeid, legacy=True
)
warnings.append(warning)
def pytest_warning_captured(self, warning_message, item):
# from _pytest.nodes import get_fslocation_from_item
from _pytest.warnings import warning_record_to_str

View File

@ -160,19 +160,19 @@ def pytest_terminal_summary(terminalreporter):
yield
def _issue_config_warning(warning, config, stacklevel):
def _issue_warning_captured(warning, hook, stacklevel):
"""
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:
: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)
config.hook.pytest_warning_captured.call_historic(
hook.pytest_warning_captured.call_historic(
kwargs=dict(warning_message=records[0], when="config", item=None)
)

View File

@ -146,6 +146,7 @@ class TestGeneralUsage(object):
assert result.ret
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])
@pytest.mark.filterwarnings("default")
def test_better_reporting_on_conftest_load_failure(self, testdir, request):
"""Show a user-friendly traceback on conftest import failures (#486, #3332)"""
testdir.makepyfile("")

View File

@ -120,6 +120,7 @@ def test_terminal_reporter_writer_attr(pytestconfig):
@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"])
@pytest.mark.filterwarnings("default")
def test_pytest_catchlog_deprecated(testdir, plugin):
testdir.makepyfile(
"""

View File

@ -823,7 +823,9 @@ def test_rewritten():
testdir.makepyfile(test_remember_rewritten_modules="")
warnings = []
hook = AssertionRewritingHook(pytestconfig)
monkeypatch.setattr(hook.config, "warn", lambda code, msg: warnings.append(msg))
monkeypatch.setattr(
hook, "_warn_already_imported", lambda code, msg: warnings.append(msg)
)
hook.find_module("test_remember_rewritten_modules")
hook.load_module("test_remember_rewritten_modules")
hook.mark_rewrite("test_remember_rewritten_modules")

View File

@ -12,7 +12,6 @@ from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import getcfg
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
class TestParseIni(object):
@ -790,66 +789,6 @@ def test_collect_pytest_prefix_bug(pytestconfig):
assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None
class TestLegacyWarning(object):
@pytest.mark.filterwarnings("default")
def test_warn_config(self, testdir):
testdir.makeconftest(
"""
values = []
def pytest_runtest_setup(item):
item.config.warn("C1", "hello")
def pytest_logwarning(code, message):
if message == "hello" and code == "C1":
values.append(1)
"""
)
testdir.makepyfile(
"""
def test_proper(pytestconfig):
import conftest
assert conftest.values == [1]
"""
)
result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG)
result.stdout.fnmatch_lines(
["*hello", "*config.warn has been deprecated*", "*1 passed*"]
)
@pytest.mark.filterwarnings("default")
@pytest.mark.parametrize("use_kw", [True, False])
def test_warn_on_test_item_from_request(self, testdir, use_kw):
code_kw = "code=" if use_kw else ""
message_kw = "message=" if use_kw else ""
testdir.makepyfile(
"""
import pytest
@pytest.fixture
def fix(request):
request.node.warn({code_kw}"T1", {message_kw}"hello")
def test_hello(fix):
pass
""".format(
code_kw=code_kw, message_kw=message_kw
)
)
result = testdir.runpytest(
"--disable-pytest-warnings", SHOW_PYTEST_WARNINGS_ARG
)
assert "hello" not in result.stdout.str()
result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG)
result.stdout.fnmatch_lines(
"""
===*warnings summary*===
*test_warn_on_test_item_from_request.py::test_hello*
*hello*
*test_warn_on_test_item_from_request.py:7:*Node.warn(code, message) form has been deprecated*
"""
)
class TestRootdir(object):
def test_simple_noini(self, tmpdir):
assert get_common_ancestor([tmpdir]) == tmpdir

View File

@ -32,7 +32,7 @@ class TestPytestPluginInteractions(object):
"""
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.addhooks(newhooks)
pluginmanager.add_hookspecs(newhooks)
def pytest_myhook(xyz):
return xyz + 1
"""
@ -52,7 +52,7 @@ class TestPytestPluginInteractions(object):
"""
import sys
def pytest_addhooks(pluginmanager):
pluginmanager.addhooks(sys)
pluginmanager.add_hookspecs(sys)
"""
)
res = testdir.runpytest()
@ -141,23 +141,6 @@ class TestPytestPluginInteractions(object):
ihook_b = session.gethookproxy(testdir.tmpdir.join("tests"))
assert ihook_a is not ihook_b
def test_warn_on_deprecated_addhooks(self, pytestpm):
warnings = []
class get_warnings(object):
def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings.append(message)
class Plugin(object):
def pytest_testhook():
pass
pytestpm.register(get_warnings())
before = list(warnings)
pytestpm.addhooks(Plugin())
assert len(warnings) == len(before) + 1
assert "deprecated" in warnings[-1]
def test_default_markers(testdir):
result = testdir.runpytest("--markers")
@ -240,11 +223,12 @@ class TestPytestPluginManager(object):
with pytest.raises(ImportError):
pytestpm.consider_env()
@pytest.mark.filterwarnings("always")
def test_plugin_skip(self, testdir, monkeypatch):
p = testdir.makepyfile(
skipping1="""
import pytest
pytest.skip("hello")
pytest.skip("hello", allow_module_level=True)
"""
)
p.copy(p.dirpath("skipping2.py"))

View File

@ -168,7 +168,7 @@ def make_holder():
@pytest.mark.parametrize("holder", make_holder())
def test_hookrecorder_basic(holder):
pm = PytestPluginManager()
pm.addhooks(holder)
pm.add_hookspecs(holder)
rec = HookRecorder(pm)
pm.hook.pytest_xyz(arg=123)
call = rec.popcall("pytest_xyz")

View File

@ -308,9 +308,9 @@ def test_filterwarnings_mark_registration(testdir):
def test_warning_captured_hook(testdir):
testdir.makeconftest(
"""
from _pytest.warnings import _issue_config_warning
from _pytest.warnings import _issue_warning_captured
def pytest_configure(config):
_issue_config_warning(UserWarning("config warning"), config, stacklevel=2)
_issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2)
"""
)
testdir.makepyfile(