From 495459dc880d1cb47cec30ae8a8be7111b3d9916 Mon Sep 17 00:00:00 2001 From: eitanwass Date: Mon, 2 Dec 2024 21:52:06 +0200 Subject: [PATCH 1/5] Fix parametrized mark over staticmethod decorator --- AUTHORS | 1 + changelog/12863.bugfix.rst | 1 + pyproject.toml | 2 +- src/_pytest/mark/structures.py | 6 +++++- testing/test_mark.py | 17 +++++++++++++++++ 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 changelog/12863.bugfix.rst 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..03b0c873ba3 --- /dev/null +++ b/changelog/12863.bugfix.rst @@ -0,0 +1 @@ +Fix :func:`pytest.mark.parametrize` marker placed above `@staticmethod` 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..c07116655a8 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -350,7 +350,11 @@ def __call__(self, *args: object, **kwargs: object): 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) + if isinstance(func, staticmethod): + # If the marker decorates a staticmethod, store on the test func + store_mark(func.__func__, self.mark, stacklevel=3) + else: + store_mark(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..9e9f87d5d31 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1226,3 +1226,20 @@ def test_attrs(self): ) result = pytester.runpytest(foo) result.assert_outcomes(passed=1) + + +# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/12863") +def test_mark_parametrize_over_staticmethod(pytester: Pytester) -> None: + foo = pytester.makepyfile( + """ + import pytest + + class TestClass: + @pytest.mark.parametrize("value", [1, 2]) + @staticmethod + def test_foo(value: int): + assert value in [1, 2] + """ + ) + result = pytester.runpytest(foo) + result.assert_outcomes(passed=2) From ec34f1c5b7a0878ce6cee1bdceab1e689f82a78e Mon Sep 17 00:00:00 2001 From: eitanwass Date: Tue, 3 Dec 2024 22:22:39 +0200 Subject: [PATCH 2/5] Fix compatibility with python pre-3.10 --- src/_pytest/mark/structures.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index c07116655a8..05208cf9b53 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -349,12 +349,9 @@ 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): - if isinstance(func, staticmethod): - # If the marker decorates a staticmethod, store on the test func - store_mark(func.__func__, self.mark, stacklevel=3) - else: - store_mark(func, self.mark, stacklevel=3) + marking_func = func.__func__ if isinstance(func, staticmethod) else func + if len(args) == 1 and (istestfunc(marking_func) or is_class): + store_mark(marking_func, self.mark, stacklevel=3) return func return self.with_args(*args, **kwargs) From 27d1a2f7da5f642aca4a73ecd4662936e4b7ea28 Mon Sep 17 00:00:00 2001 From: eitanwass Date: Wed, 4 Dec 2024 19:37:29 +0200 Subject: [PATCH 3/5] Get any contained func when marking --- changelog/12863.bugfix.rst | 2 +- src/_pytest/mark/structures.py | 2 +- testing/test_mark.py | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/changelog/12863.bugfix.rst b/changelog/12863.bugfix.rst index 03b0c873ba3..ad4c6dbf4a3 100644 --- a/changelog/12863.bugfix.rst +++ b/changelog/12863.bugfix.rst @@ -1 +1 @@ -Fix :func:`pytest.mark.parametrize` marker placed above `@staticmethod` +Fix :func:`pytest.mark.parametrize ` marker placed above `@staticmethod` diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 05208cf9b53..bbdd4838610 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -349,7 +349,7 @@ def __call__(self, *args: object, **kwargs: object): if args and not kwargs: func = args[0] is_class = inspect.isclass(func) - marking_func = func.__func__ if isinstance(func, staticmethod) else func + marking_func = getattr(func, "__func__", func) if len(args) == 1 and (istestfunc(marking_func) or is_class): store_mark(marking_func, self.mark, stacklevel=3) return func diff --git a/testing/test_mark.py b/testing/test_mark.py index 9e9f87d5d31..dd132920217 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1235,11 +1235,16 @@ def test_mark_parametrize_over_staticmethod(pytester: Pytester) -> None: import pytest class TestClass: + @pytest.mark.parametrize("value", [1, 2]) + @classmethod + def test_classmethod_wrapper(cls, value: int): + assert value in [1, 2] + @pytest.mark.parametrize("value", [1, 2]) @staticmethod - def test_foo(value: int): + def test_staticmethod_wrapper(value: int): assert value in [1, 2] """ ) result = pytester.runpytest(foo) - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=4) From ec60713bfe223931abe7c049326e2d30d9efb21d Mon Sep 17 00:00:00 2001 From: eitanwass Date: Thu, 5 Dec 2024 19:05:42 +0200 Subject: [PATCH 4/5] Additional sanity tests, and fixes --- changelog/12863.bugfix.rst | 2 +- testing/test_mark.py | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/changelog/12863.bugfix.rst b/changelog/12863.bugfix.rst index ad4c6dbf4a3..97a26464bf3 100644 --- a/changelog/12863.bugfix.rst +++ b/changelog/12863.bugfix.rst @@ -1 +1 @@ -Fix :func:`pytest.mark.parametrize ` marker placed above `@staticmethod` +Fix :ref:`pytest.mark.parametrize ` marker placed above `@staticmethod` diff --git a/testing/test_mark.py b/testing/test_mark.py index dd132920217..c8109ae3a6c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1230,7 +1230,11 @@ def test_attrs(self): # @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/12863") def test_mark_parametrize_over_staticmethod(pytester: Pytester) -> None: - foo = pytester.makepyfile( + """Check that applying marks works as intended on classmethods and staticmethods. + + Regression test for #12863. + """ + pytester.makepyfile( """ import pytest @@ -1240,11 +1244,21 @@ class TestClass: 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(foo) - result.assert_outcomes(passed=4) + result = pytester.runpytest() + result.assert_outcomes(passed=8) From 37cf65912906b7402dbc7134ba4344f36243b931 Mon Sep 17 00:00:00 2001 From: eitanwass Date: Fri, 6 Dec 2024 08:38:44 +0200 Subject: [PATCH 5/5] More explicit edgecase of decorators --- changelog/12863.bugfix.rst | 2 +- src/_pytest/mark/structures.py | 10 +++++++--- testing/test_mark.py | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog/12863.bugfix.rst b/changelog/12863.bugfix.rst index 97a26464bf3..0b1c397a08e 100644 --- a/changelog/12863.bugfix.rst +++ b/changelog/12863.bugfix.rst @@ -1 +1 @@ -Fix :ref:`pytest.mark.parametrize ` marker placed above `@staticmethod` +Fix applying markers, including :ref:`pytest.mark.parametrize ` when placed above `@staticmethod` or `@classmethod`. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index bbdd4838610..624b37cab94 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -349,9 +349,13 @@ def __call__(self, *args: object, **kwargs: object): if args and not kwargs: func = args[0] is_class = inspect.isclass(func) - marking_func = getattr(func, "__func__", func) - if len(args) == 1 and (istestfunc(marking_func) or is_class): - store_mark(marking_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 c8109ae3a6c..60ee795cf43 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1228,7 +1228,6 @@ def test_attrs(self): result.assert_outcomes(passed=1) -# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/12863") def test_mark_parametrize_over_staticmethod(pytester: Pytester) -> None: """Check that applying marks works as intended on classmethods and staticmethods.