diff --git a/AUTHORS b/AUTHORS index 3419accfa6b..9629e00bcfb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -150,6 +150,7 @@ Eric Yuan Erik Aronesty Erik Hasse Erik M. Bray +Ethan Wass Evan Kepner Evgeny Seliverstov Fabian Sturm diff --git a/changelog/12863.bugfix.rst b/changelog/12863.bugfix.rst new file mode 100644 index 00000000000..0b1c397a08e --- /dev/null +++ b/changelog/12863.bugfix.rst @@ -0,0 +1 @@ +Fix applying markers, including :ref:`pytest.mark.parametrize ` when placed above `@staticmethod` or `@classmethod`. diff --git a/pyproject.toml b/pyproject.toml index dce6a0870e1..0a695e0247e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -334,7 +334,7 @@ disable = [ [tool.codespell] ignore-words-list = "afile,asend,asser,assertio,feld,hove,ned,noes,notin,paramete,parth,socio-economic,tesults,varius,wil" -skip = "*/plugin_list.rst" +skip = "AUTHORS,*/plugin_list.rst" write-changes = true [tool.check-wheel-contents] diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index ac64ef2d606..624b37cab94 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -349,8 +349,13 @@ def __call__(self, *args: object, **kwargs: object): if args and not kwargs: 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) + # For staticmethods/classmethods, the marks are eventually fetched from the + # function object, not the descriptor, so unwrap. + unwrapped_func = func + if isinstance(func, (staticmethod, classmethod)): + unwrapped_func = func.__func__ + if len(args) == 1 and (istestfunc(unwrapped_func) or is_class): + store_mark(unwrapped_func, self.mark, stacklevel=3) return func return self.with_args(*args, **kwargs) diff --git a/testing/test_mark.py b/testing/test_mark.py index 89eef7920cf..60ee795cf43 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1226,3 +1226,38 @@ def test_attrs(self): ) result = pytester.runpytest(foo) result.assert_outcomes(passed=1) + + +def test_mark_parametrize_over_staticmethod(pytester: Pytester) -> None: + """Check that applying marks works as intended on classmethods and staticmethods. + + Regression test for #12863. + """ + pytester.makepyfile( + """ + import pytest + + class TestClass: + @pytest.mark.parametrize("value", [1, 2]) + @classmethod + def test_classmethod_wrapper(cls, value: int): + assert value in [1, 2] + + @classmethod + @pytest.mark.parametrize("value", [1, 2]) + def test_classmethod_wrapper_on_top(cls, value: int): + assert value in [1, 2] + + @pytest.mark.parametrize("value", [1, 2]) + @staticmethod + def test_staticmethod_wrapper(value: int): + assert value in [1, 2] + + @staticmethod + @pytest.mark.parametrize("value", [1, 2]) + def test_staticmethod_wrapper_on_top(value: int): + assert value in [1, 2] + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=8)