From 9733d57beddc994655018bee7ef15d33fdff1c20 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 2 Jul 2024 18:25:59 -0400 Subject: [PATCH] ENH: Figure out where to go --- src/_pytest/deprecated.py | 25 +++++++++++++++++++++++++ src/_pytest/fixtures.py | 5 +++-- src/_pytest/mark/structures.py | 11 +++++------ testing/deprecated_test.py | 14 +++++++++++++- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index a605c24e5..1fefdff41 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -11,7 +11,10 @@ in case of warnings which need to format their messages. from __future__ import annotations +import inspect +from pathlib import Path from warnings import warn +from warnings import warn_explicit from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestRemovedIn9Warning @@ -89,3 +92,25 @@ MARKED_FIXTURE = PytestRemovedIn9Warning( def check_ispytest(ispytest: bool) -> None: if not ispytest: warn(PRIVATE, stacklevel=3) + + +def _warn_auto_stacklevel(message, category=UserWarning): + """Emit a warning with trace outside the pytest namespace.""" + root_dir = Path(__file__).parents[1] + frame = inspect.currentframe() + fname, lineno = "unknown", 0 + while frame: + fname = frame.f_code.co_filename + lineno = frame.f_lineno + if fname and root_dir not in Path(fname).parents: + break + frame = frame.f_back + del frame + warn_explicit( + message, + category, + filename=fname, + lineno=lineno, + module="pytest", + registry=globals().get("__warningregistry__", {}), + ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1f0740d09..5c487e0b7 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -60,6 +60,7 @@ from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE +from _pytest.deprecated import _warn_auto_stacklevel from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import ParameterSet @@ -1192,7 +1193,7 @@ class FixtureFunctionMarker: ) if hasattr(function, "pytestmark"): - warnings.warn(MARKED_FIXTURE, stacklevel=4) + _warn_auto_stacklevel(MARKED_FIXTURE) function = wrap_function_to_error_out_if_called_directly(function, self) @@ -1322,7 +1323,7 @@ def yield_fixture( .. deprecated:: 3.0 Use :py:func:`pytest.fixture` directly instead. """ - warnings.warn(YIELD_FIXTURE, stacklevel=2) + _warn_auto_stacklevel(YIELD_FIXTURE) return fixture( fixture_function, *args, diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 92ade55f7..74344f199 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -18,7 +18,6 @@ from typing import Sequence from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import warnings from .._code import getfslineno from ..compat import ascii_escaped @@ -27,6 +26,7 @@ from ..compat import NotSetType from _pytest.config import Config from _pytest.deprecated import check_ispytest from _pytest.deprecated import MARKED_FIXTURE +from _pytest.deprecated import _warn_auto_stacklevel from _pytest.outcomes import fail from _pytest.scope import _ScopeName from _pytest.warning_types import PytestUnknownMarkWarning @@ -353,7 +353,7 @@ class MarkDecorator: func = args[0] is_class = inspect.isclass(func) if len(args) == 1 and (istestfunc(func) or is_class): - store_mark(func, self.mark, stacklevel=3) + store_mark(func, self.mark) return func return self.with_args(*args, **kwargs) @@ -408,7 +408,7 @@ def normalize_mark_list( yield mark_obj -def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: +def store_mark(obj, mark: Mark) -> None: """Store a Mark on an object. This is used to implement the Mark declarations/decorators correctly. @@ -418,7 +418,7 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: from ..fixtures import getfixturemarker if getfixturemarker(obj) is not None: - warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel) + _warn_auto_stacklevel(MARKED_FIXTURE) # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. @@ -543,12 +543,11 @@ class MarkGenerator: __tracebackhide__ = True fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") - warnings.warn( + _warn_auto_stacklevel( f"Unknown pytest.mark.{name} - is this a typo? You can register " "custom marks to avoid this warning - for details, see " "https://docs.pytest.org/en/stable/how-to/mark.html", PytestUnknownMarkWarning, - 2, ) return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 0fd2e9510..bb748aa64 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -59,7 +59,7 @@ def test_hookimpl_via_function_attributes_are_deprecated(): with pytest.warns( PytestDeprecationWarning, - match=r"Please use the pytest.hookimpl\(tryfirst=True\)", + match=r"Please use the pytest\.hookimpl\(tryfirst=True\)", ) as recorder: pm.register(DeprecatedMarkImplPlugin()) (record,) = recorder @@ -188,6 +188,18 @@ def test_fixture_disallow_on_marked_functions(): # should point to this file assert record[0].filename == __file__ + # Same with a different order + with pytest.warns( + pytest.PytestRemovedIn9Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + @pytest.mark.parametrize("example", ["hello"]) + @pytest.fixture + def foo(): + raise NotImplementedError() + assert len(record) == 1 + assert record[0].filename == __file__ + def test_fixture_disallow_marks_on_fixtures(): """Test that applying a mark to a fixture warns (#3364)."""