From 8532e991a53732a241f0d2b4b1f254a6db597b69 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:06:01 -0300 Subject: [PATCH 1/3] Publish UnknownMarkWarning as part of the public API and docs --- doc/en/warnings.rst | 2 ++ src/_pytest/mark/structures.py | 4 ++-- src/_pytest/warning_types.py | 2 +- src/pytest.py | 2 ++ tox.ini | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 26bd2fdb2..54740b970 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -420,3 +420,5 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.RemovedInPytest4Warning .. autoclass:: pytest.PytestExperimentalApiWarning + +.. autoclass:: pytest.PytestUnknownMarkWarning diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 4cae97b71..b4ff6e988 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -12,7 +12,7 @@ from ..compat import MappingMixin from ..compat import NOTSET from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail -from _pytest.warning_types import UnknownMarkWarning +from _pytest.warning_types import PytestUnknownMarkWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -318,7 +318,7 @@ class MarkGenerator(object): "Unknown pytest.mark.%s - is this a typo? You can register " "custom marks to avoid this warning - for details, see " "https://docs.pytest.org/en/latest/mark.html" % name, - UnknownMarkWarning, + PytestUnknownMarkWarning, ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index ffc6e69d6..70f41a2b2 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -9,7 +9,7 @@ class PytestWarning(UserWarning): """ -class UnknownMarkWarning(PytestWarning): +class PytestUnknownMarkWarning(PytestWarning): """ Bases: :class:`PytestWarning`. diff --git a/src/pytest.py b/src/pytest.py index c0010f166..2be72ce77 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -37,6 +37,7 @@ from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning from _pytest.warning_types import RemovedInPytest4Warning @@ -72,6 +73,7 @@ __all__ = [ "raises", "register_assert_rewrite", "RemovedInPytest4Warning", + "PytestUnknownMarkWarning", "Session", "set_trace", "skip", diff --git a/tox.ini b/tox.ini index e297b7099..3c1ca65b7 100644 --- a/tox.ini +++ b/tox.ini @@ -169,7 +169,7 @@ filterwarnings = # Do not cause SyntaxError for invalid escape sequences in py37. default:invalid escape sequence:DeprecationWarning # ignore use of unregistered marks, because we use many to test the implementation - ignore::_pytest.warning_types.UnknownMarkWarning + ignore::_pytest.warning_types.PytestUnknownMarkWarning pytester_example_dir = testing/example_scripts markers = issue From 53cd7fd2ea2cba07820009a4cfba1e09fa35fcfa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:38:25 -0300 Subject: [PATCH 2/3] Introduce new warning subclasses Fix #5177 --- doc/en/warnings.rst | 14 ++++++- src/_pytest/assertion/rewrite.py | 16 +++++--- src/_pytest/cacheprovider.py | 4 +- src/_pytest/config/__init__.py | 8 ++-- src/_pytest/junitxml.py | 6 ++- src/_pytest/python.py | 13 ++++--- src/_pytest/warning_types.py | 64 ++++++++++++++++++++++++++------ src/pytest.py | 12 +++++- testing/test_warnings.py | 2 +- 9 files changed, 104 insertions(+), 35 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 54740b970..83d2d6b15 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -415,10 +415,20 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.PytestWarning -.. autoclass:: pytest.PytestDeprecationWarning +.. autoclass:: pytest.PytestAssertRewriteWarning -.. autoclass:: pytest.RemovedInPytest4Warning +.. autoclass:: pytest.PytestCacheWarning + +.. autoclass:: pytest.PytestCollectionWarning + +.. autoclass:: pytest.PytestConfigWarning + +.. autoclass:: pytest.PytestDeprecationWarning .. autoclass:: pytest.PytestExperimentalApiWarning +.. autoclass:: pytest.PytestUnhandledCoroutineWarning + .. autoclass:: pytest.PytestUnknownMarkWarning + +.. autoclass:: pytest.RemovedInPytest4Warning diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 7cb1acb2e..24d96b722 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -268,11 +268,13 @@ class AssertionRewritingHook(object): self._marked_for_rewrite_cache.clear() def _warn_already_imported(self, name): - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning("Module already imported so cannot be rewritten: %s" % name), + PytestAssertRewriteWarning( + "Module already imported so cannot be rewritten: %s" % name + ), self.config.hook, stacklevel=5, ) @@ -819,11 +821,13 @@ class AssertionRewriter(ast.NodeVisitor): """ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestAssertRewriteWarning import warnings warnings.warn_explicit( - PytestWarning("assertion is always true, perhaps remove parentheses?"), + PytestAssertRewriteWarning( + "assertion is always true, perhaps remove parentheses?" + ), category=None, filename=str(self.module_path), lineno=assert_.lineno, @@ -887,10 +891,10 @@ class AssertionRewriter(ast.NodeVisitor): val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) send_warning = ast.parse( """ -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestAssertRewriteWarning from warnings import warn_explicit warn_explicit( - PytestWarning('asserting the value None, please use "assert is None"'), + PytestAssertRewriteWarning('asserting the value None, please use "assert is None"'), category=None, filename={filename!r}, lineno={lineno}, diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 63503ed2e..1b7001c0a 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -60,10 +60,10 @@ class Cache(object): def warn(self, fmt, **args): from _pytest.warnings import _issue_warning_captured - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestCacheWarning _issue_warning_captured( - PytestWarning(fmt.format(**args) if args else fmt), + PytestCacheWarning(fmt.format(**args) if args else fmt), self._config.hook, stacklevel=3, ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1a2edf4f8..03769b815 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -32,7 +32,7 @@ from _pytest.compat import lru_cache from _pytest.compat import safe_str from _pytest.outcomes import fail from _pytest.outcomes import Skipped -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -307,7 +307,7 @@ class PytestPluginManager(PluginManager): def register(self, plugin, name=None): if name in ["pytest_catchlog", "pytest_capturelog"]: warnings.warn( - PytestWarning( + PytestConfigWarning( "{} plugin has been merged into the core, " "please remove it from your requirements.".format( name.replace("_", "-") @@ -574,7 +574,7 @@ class PytestPluginManager(PluginManager): from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning("skipped plugin %r: %s" % (modname, e.msg)), + PytestConfigWarning("skipped plugin %r: %s" % (modname, e.msg)), self.hook, stacklevel=1, ) @@ -863,7 +863,7 @@ class Config(object): from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning( + PytestConfigWarning( "could not load initial conftests: {}".format(e.path) ), self.hook, diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index c2b277b8a..3a5f31735 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -307,9 +307,11 @@ def record_xml_attribute(request): The fixture is callable with ``(name, value)``, with value being automatically xml-encoded """ - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestExperimentalApiWarning, PytestWarning - request.node.warn(PytestWarning("record_xml_attribute is an experimental feature")) + request.node.warn( + PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") + ) # Declare noop def add_attr_noop(name, value): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 135f5bff9..377357846 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -45,7 +45,8 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.pathlib import parts -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestUnhandledCoroutineWarning def pyobj_property(name): @@ -171,7 +172,7 @@ def pytest_pyfunc_call(pyfuncitem): msg += " - pytest-asyncio\n" msg += " - pytest-trio\n" msg += " - pytest-tornasync" - warnings.warn(PytestWarning(msg.format(pyfuncitem.nodeid))) + warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) skip(msg="coroutine function and no async plugin installed (see warnings)") funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} @@ -221,7 +222,7 @@ def pytest_pycollect_makeitem(collector, name, obj): if not (isfunction(obj) or isfunction(get_real_func(obj))): filename, lineno = getfslineno(obj) warnings.warn_explicit( - message=PytestWarning( + message=PytestCollectionWarning( "cannot collect %r because it is not a function." % name ), category=None, @@ -233,7 +234,7 @@ def pytest_pycollect_makeitem(collector, name, obj): res = Function(name, parent=collector) reason = deprecated.YIELD_TESTS.format(name=name) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) - res.warn(PytestWarning(reason)) + res.warn(PytestCollectionWarning(reason)) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res) @@ -721,7 +722,7 @@ class Class(PyCollector): return [] if hasinit(self.obj): self.warn( - PytestWarning( + PytestCollectionWarning( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__ ) @@ -729,7 +730,7 @@ class Class(PyCollector): return [] elif hasnew(self.obj): self.warn( - PytestWarning( + PytestCollectionWarning( "cannot collect test class %r because it has a " "__new__ constructor" % self.obj.__name__ ) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 70f41a2b2..2777aabea 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -9,12 +9,35 @@ class PytestWarning(UserWarning): """ -class PytestUnknownMarkWarning(PytestWarning): +class PytestAssertRewriteWarning(PytestWarning): """ Bases: :class:`PytestWarning`. - Warning emitted on use of unknown markers. - See https://docs.pytest.org/en/latest/mark.html for details. + Warning emitted by the pytest assert rewrite module. + """ + + +class PytestCacheWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted by the cache plugin in various situations. + """ + + +class PytestConfigWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted for configuration issues. + """ + + +class PytestCollectionWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted when pytest is not able to collect a file or symbol in a module. """ @@ -26,14 +49,6 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """ -class RemovedInPytest4Warning(PytestDeprecationWarning): - """ - Bases: :class:`pytest.PytestDeprecationWarning`. - - Warning class for features scheduled to be removed in pytest 4.0. - """ - - class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """ Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. @@ -51,6 +66,33 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): ) +class PytestUnhandledCoroutineWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted when pytest encounters a test function which is a coroutine, + but it was not handled by any async-aware plugin. Coroutine test functions + are not natively supported. + """ + + +class PytestUnknownMarkWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted on use of unknown markers. + See https://docs.pytest.org/en/latest/mark.html for details. + """ + + +class RemovedInPytest4Warning(PytestDeprecationWarning): + """ + Bases: :class:`pytest.PytestDeprecationWarning`. + + Warning class for features scheduled to be removed in pytest 4.0. + """ + + @attr.s class UnformattedWarning(object): """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. diff --git a/src/pytest.py b/src/pytest.py index 2be72ce77..a6376843d 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -35,8 +35,13 @@ from _pytest.python_api import approx from _pytest.python_api import raises from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns +from _pytest.warning_types import PytestAssertRewriteWarning +from _pytest.warning_types import PytestCacheWarning +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning from _pytest.warning_types import RemovedInPytest4Warning @@ -67,13 +72,18 @@ __all__ = [ "Module", "Package", "param", + "PytestAssertRewriteWarning", + "PytestCacheWarning", + "PytestCollectionWarning", + "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", + "PytestUnhandledCoroutineWarning", + "PytestUnknownMarkWarning", "PytestWarning", "raises", "register_assert_rewrite", "RemovedInPytest4Warning", - "PytestUnknownMarkWarning", "Session", "set_trace", "skip", diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 929ae8d60..a24289f57 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -630,7 +630,7 @@ def test_removed_in_pytest4_warning_as_error(testdir, change_default): class TestAssertionWarnings: @staticmethod def assert_result_warns(result, msg): - result.stdout.fnmatch_lines(["*PytestWarning: %s*" % msg]) + result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg]) def test_tuple_warning(self, testdir): testdir.makepyfile( From 915ecb0dac19f05f7a392161203539076bce7d5c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:41:44 -0300 Subject: [PATCH 3/3] Add CHANGELOG for #5177 --- changelog/5177.feature.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 changelog/5177.feature.rst diff --git a/changelog/5177.feature.rst b/changelog/5177.feature.rst new file mode 100644 index 000000000..a5b4ab111 --- /dev/null +++ b/changelog/5177.feature.rst @@ -0,0 +1,14 @@ +Introduce new specific warning ``PytestWarning`` subclasses to make it easier to filter warnings based on the class, rather than on the message. The new subclasses are: + + +* ``PytestAssertRewriteWarning`` + +* ``PytestCacheWarning`` + +* ``PytestCollectionWarning`` + +* ``PytestConfigWarning`` + +* ``PytestUnhandledCoroutineWarning`` + +* ``PytestUnknownMarkWarning``