From da25c201ab056bee1f86d4e0d7ae535650fbc681 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 21:22:09 +0330 Subject: [PATCH 01/18] Show return type annotations in fixtures --- src/_pytest/fixtures.py | 17 +++++++++++++++++ testing/python/fixtures.py | 28 +++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 91f1b3a67f6..b46a5c0cd1e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1911,6 +1911,9 @@ def write_fixture(fixture_def: FixtureDef[object]) -> None: return prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) tw.write(f"{argname}", green=True) + ret_annotation = get_return_annotation(fixture_def) + if ret_annotation: + tw.write(f" -> {ret_annotation}", cyan=True) tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") fixture_doc = inspect.getdoc(fixture_def.func) @@ -1995,6 +1998,9 @@ def _showfixtures_main(config: Config, session: Session) -> None: if verbose <= 0 and argname.startswith("_"): continue tw.write(f"{argname}", green=True) + ret_annotation = get_return_annotation(fixturedef) + if ret_annotation: + tw.write(f" -> {ret_annotation}", cyan=True) if fixturedef.scope != "function": tw.write(f" [{fixturedef.scope} scope]", cyan=True) tw.write(f" -- {prettypath}", yellow=True) @@ -2009,6 +2015,17 @@ def _showfixtures_main(config: Config, session: Session) -> None: tw.line() +def get_return_annotation(fixturedef: FixtureDef[object]) -> str: + try: + sig = signature(fixturedef.func) + annotation = sig.return_annotation + if annotation is not sig.empty and annotation != inspect._empty: + return inspect.formatannotation(annotation) + except (ValueError, TypeError): + pass + return "" + + def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): tw.line(indent + line) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8b97d35c21e..5ccd483c4f7 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3582,9 +3582,9 @@ def test_show_fixtures(self, pytester: Pytester) -> None: result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( [ - "tmp_path_factory [[]session scope[]] -- .../_pytest/tmpdir.py:*", + "tmp_path_factory* [[]session scope[]] -- .../_pytest/tmpdir.py:*", "*for the test session*", - "tmp_path -- .../_pytest/tmpdir.py:*", + "tmp_path* -- .../_pytest/tmpdir.py:*", "*temporary directory*", ] ) @@ -3593,9 +3593,9 @@ def test_show_fixtures_verbose(self, pytester: Pytester) -> None: result = pytester.runpytest("--fixtures", "-v") result.stdout.fnmatch_lines( [ - "tmp_path_factory [[]session scope[]] -- .../_pytest/tmpdir.py:*", + "tmp_path_factory* [[]session scope[]] -- .../_pytest/tmpdir.py:*", "*for the test session*", - "tmp_path -- .../_pytest/tmpdir.py:*", + "tmp_path* -- .../_pytest/tmpdir.py:*", "*temporary directory*", ] ) @@ -3615,7 +3615,7 @@ def arg1(): result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( """ - *tmp_path -- * + *tmp_path* -- * *fixtures defined from* *arg1 -- test_show_fixtures_testmodule.py:6* *hello world* @@ -3623,6 +3623,24 @@ def arg1(): ) result.stdout.no_fnmatch_line("*arg0*") + def test_show_fixtures_return_annotation(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + ''' + import pytest + @pytest.fixture + def six() -> int: + return 6 + ''' + ) + result = pytester.runpytest("--fixtures", p) + result.stdout.fnmatch_lines( + """ + *tmp_path* -- * + *fixtures defined from* + *six -> int -- test_show_fixtures_return_annotation.py:3* + """ + ) + @pytest.mark.parametrize("testmod", [True, False]) def test_show_fixtures_conftest(self, pytester: Pytester, testmod) -> None: pytester.makeconftest( From b28d1309ad56ab7ece05b2a67df17bc48fbc93dc Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 21:27:05 +0330 Subject: [PATCH 02/18] Add changelog entry --- changelog/13676.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/13676.improvement.rst diff --git a/changelog/13676.improvement.rst b/changelog/13676.improvement.rst new file mode 100644 index 00000000000..5a0ac6cfc95 --- /dev/null +++ b/changelog/13676.improvement.rst @@ -0,0 +1 @@ +Added return type annotations in ``fixtures`` and ``fixtures-per-test``. From b5a86c769590e6303d800d5c19ec3c876b8a544d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:02:02 +0000 Subject: [PATCH 03/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 5ccd483c4f7..64349db1c79 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3625,12 +3625,12 @@ def arg1(): def test_show_fixtures_return_annotation(self, pytester: Pytester) -> None: p = pytester.makepyfile( - ''' + """ import pytest @pytest.fixture def six() -> int: return 6 - ''' + """ ) result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( From 37732a0e85dfd925bd250940b0f681e776059410 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 22:05:15 +0330 Subject: [PATCH 04/18] Add tests for get_return_annotation --- src/_pytest/fixtures.py | 10 +++++----- testing/python/fixtures.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b46a5c0cd1e..14d2c54da04 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1911,7 +1911,7 @@ def write_fixture(fixture_def: FixtureDef[object]) -> None: return prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) tw.write(f"{argname}", green=True) - ret_annotation = get_return_annotation(fixture_def) + ret_annotation = get_return_annotation(fixture_def.func) if ret_annotation: tw.write(f" -> {ret_annotation}", cyan=True) tw.write(f" -- {prettypath}", yellow=True) @@ -1998,7 +1998,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if verbose <= 0 and argname.startswith("_"): continue tw.write(f"{argname}", green=True) - ret_annotation = get_return_annotation(fixturedef) + ret_annotation = get_return_annotation(fixturedef.func) if ret_annotation: tw.write(f" -> {ret_annotation}", cyan=True) if fixturedef.scope != "function": @@ -2015,12 +2015,12 @@ def _showfixtures_main(config: Config, session: Session) -> None: tw.line() -def get_return_annotation(fixturedef: FixtureDef[object]) -> str: +def get_return_annotation(fixture_func: Callable) -> str: try: - sig = signature(fixturedef.func) + sig = signature(fixture_func) annotation = sig.return_annotation if annotation is not sig.empty and annotation != inspect._empty: - return inspect.formatannotation(annotation) + return inspect.formatannotation(annotation).replace("'", "") except (ValueError, TypeError): pass return "" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 64349db1c79..9fd64304555 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6,10 +6,12 @@ from pathlib import Path import sys import textwrap +from typing import Tuple from _pytest.compat import getfuncargnames from _pytest.config import ExitCode from _pytest.fixtures import deduplicate_names +from _pytest.fixtures import get_return_annotation from _pytest.fixtures import TopRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names @@ -3635,7 +3637,6 @@ def six() -> int: result = pytester.runpytest("--fixtures", p) result.stdout.fnmatch_lines( """ - *tmp_path* -- * *fixtures defined from* *six -> int -- test_show_fixtures_return_annotation.py:3* """ @@ -5087,3 +5088,20 @@ def test_method(self, /, fix): ) result = pytester.runpytest() result.assert_outcomes(passed=1) + +def test_get_return_annotation() -> None: + def six() -> int: + return 6 + assert get_return_annotation(six) == "int" + + def two_sixes() -> Tuple[int, str]: + return (6, "six") + assert get_return_annotation(two_sixes) == "Tuple[int, str]" + + def no_annot(): + return 6 + assert get_return_annotation(no_annot) == "" + + def none_return() -> None: + pass + assert get_return_annotation(none_return) == "None" From 2efc48e335047e0fa2ff363727a342ef90263dca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:35:45 +0000 Subject: [PATCH 05/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 9fd64304555..6955f48caf5 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6,7 +6,6 @@ from pathlib import Path import sys import textwrap -from typing import Tuple from _pytest.compat import getfuncargnames from _pytest.config import ExitCode @@ -5089,19 +5088,24 @@ def test_method(self, /, fix): result = pytester.runpytest() result.assert_outcomes(passed=1) + def test_get_return_annotation() -> None: def six() -> int: return 6 + assert get_return_annotation(six) == "int" - def two_sixes() -> Tuple[int, str]: + def two_sixes() -> tuple[int, str]: return (6, "six") + assert get_return_annotation(two_sixes) == "Tuple[int, str]" def no_annot(): return 6 + assert get_return_annotation(no_annot) == "" def none_return() -> None: pass + assert get_return_annotation(none_return) == "None" From ec239a44ba4694ff09dca96aa1dc35cc588fa04c Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 22:08:08 +0330 Subject: [PATCH 06/18] Update get_return_annotation test --- src/_pytest/fixtures.py | 2 +- testing/python/fixtures.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 14d2c54da04..fe13769f453 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2015,7 +2015,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: tw.line() -def get_return_annotation(fixture_func: Callable) -> str: +def get_return_annotation(fixture_func: Callable[..., Any]) -> str: try: sig = signature(fixture_func) annotation = sig.return_annotation diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6955f48caf5..20c17787b37 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5098,7 +5098,7 @@ def six() -> int: def two_sixes() -> tuple[int, str]: return (6, "six") - assert get_return_annotation(two_sixes) == "Tuple[int, str]" + assert get_return_annotation(two_sixes) == "tuple[int, str]" def no_annot(): return 6 @@ -5109,3 +5109,5 @@ def none_return() -> None: pass assert get_return_annotation(none_return) == "None" + + assert get_return_annotation(range) == "" From bc6450dfaf12073b9121875e8634a3df81d879c3 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 28 Aug 2025 12:11:58 +0330 Subject: [PATCH 07/18] Improve get_return_annotation logic --- changelog/13676.improvement.rst | 2 +- src/_pytest/fixtures.py | 8 ++++++-- testing/python/fixtures.py | 22 ++++++++++++++++++++-- testing/python/show_fixtures_per_test.py | 24 ++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/changelog/13676.improvement.rst b/changelog/13676.improvement.rst index 5a0ac6cfc95..fc79a1a6de0 100644 --- a/changelog/13676.improvement.rst +++ b/changelog/13676.improvement.rst @@ -1 +1 @@ -Added return type annotations in ``fixtures`` and ``fixtures-per-test``. +Return type annotations are now shown in ``--fixtures`` and ``--fixtures-per-test``. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fe13769f453..c13bad1a299 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2019,8 +2019,12 @@ def get_return_annotation(fixture_func: Callable[..., Any]) -> str: try: sig = signature(fixture_func) annotation = sig.return_annotation - if annotation is not sig.empty and annotation != inspect._empty: - return inspect.formatannotation(annotation).replace("'", "") + if annotation is not sig.empty: + if isinstance(annotation, str): + return annotation + if annotation.__module__ == "typing": + return str(annotation).replace("typing.", "") + return annotation.__name__ except (ValueError, TypeError): pass return "" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 20c17787b37..85330e5631f 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6,6 +6,7 @@ from pathlib import Path import sys import textwrap +from typing import Any, Callable from _pytest.compat import getfuncargnames from _pytest.config import ExitCode @@ -5100,14 +5101,31 @@ def two_sixes() -> tuple[int, str]: assert get_return_annotation(two_sixes) == "tuple[int, str]" - def no_annot(): + def callable_return() -> Callable[..., Any]: + return two_sixes + + assert get_return_annotation(callable_return) == "Callable[..., Any]" + + def no_annotation(): return 6 - assert get_return_annotation(no_annot) == "" + assert get_return_annotation(no_annotation) == "" def none_return() -> None: pass assert get_return_annotation(none_return) == "None" + class T: + pass + def class_return() -> T: + return T() + + assert get_return_annotation(class_return) == "T" + + def enum_return() -> ExitCode: + return ExitCode(0) + + assert get_return_annotation(enum_return) == "ExitCode" + assert get_return_annotation(range) == "" diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index c860b61e21b..39b84d356ad 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -160,6 +160,30 @@ def test_args(arg2, arg3): ) +def test_show_return_annotation(pytester: Pytester) -> None: + p = pytester.makepyfile( + ''' + import pytest + @pytest.fixture + def five() -> int: + return 5 + def test_five(five): + pass + ''' + ) + + result = pytester.runpytest("--fixtures-per-test", p) + assert result.ret == 0 + + result.stdout.fnmatch_lines( + [ + "*fixtures used by test_five*", + "*(test_show_return_annotation.py:6)*", + "five -> int -- test_show_return_annotation.py:3", + ] + ) + + def test_doctest_items(pytester: Pytester) -> None: pytester.makepyfile( ''' From f4a9ceda8f2e09ac4d239d8e86d6d7b74c0f0189 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:44:10 +0000 Subject: [PATCH 08/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 8 +++++--- testing/python/show_fixtures_per_test.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 85330e5631f..bfe48dbaf81 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -6,7 +6,8 @@ from pathlib import Path import sys import textwrap -from typing import Any, Callable +from typing import Any +from typing import Callable from _pytest.compat import getfuncargnames from _pytest.config import ExitCode @@ -5118,14 +5119,15 @@ def none_return() -> None: class T: pass + def class_return() -> T: return T() - + assert get_return_annotation(class_return) == "T" def enum_return() -> ExitCode: return ExitCode(0) - + assert get_return_annotation(enum_return) == "ExitCode" assert get_return_annotation(range) == "" diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index 39b84d356ad..43289a37a7c 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -162,14 +162,14 @@ def test_args(arg2, arg3): def test_show_return_annotation(pytester: Pytester) -> None: p = pytester.makepyfile( - ''' + """ import pytest @pytest.fixture def five() -> int: return 5 def test_five(five): pass - ''' + """ ) result = pytester.runpytest("--fixtures-per-test", p) From 68acade953b26174f17ba2141566896b71a16733 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 28 Aug 2025 22:30:40 +0330 Subject: [PATCH 09/18] Move get_return_annotation tests into a separate class --- src/_pytest/fixtures.py | 4 +- testing/python/fixtures.py | 88 ++++++++++++++++++++------------------ 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c13bad1a299..eb4969e0ab3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2020,11 +2020,13 @@ def get_return_annotation(fixture_func: Callable[..., Any]) -> str: sig = signature(fixture_func) annotation = sig.return_annotation if annotation is not sig.empty: + if type(annotation) == type(None): + return "None" if isinstance(annotation, str): return annotation if annotation.__module__ == "typing": return str(annotation).replace("typing.", "") - return annotation.__name__ + return str(annotation.__name__) except (ValueError, TypeError): pass return "" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bfe48dbaf81..d5ca4196149 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4584,6 +4584,52 @@ def test_1(self, myfix): reprec.assertoutcome(passed=1) +class TestGetReturnAnnotation: + def test_primitive_return_type(self): + def six() -> int: + return 6 + + assert get_return_annotation(six) == "int" + + def test_compound_return_type(self): + def two_sixes() -> tuple[int, str]: + return (6, "six") + + assert get_return_annotation(two_sixes) == "tuple[int, str]" + + def test_callable_return_type(self): + def callable_return() -> Callable[..., Any]: + return self.test_compound_return_type + + assert get_return_annotation(callable_return) == "Callable[..., Any]" + + def test_no_annotation(self): + def no_annotation(): + return 6 + + assert get_return_annotation(no_annotation) == "" + + def test_none_return_type(self): + def none_return() -> None: + pass + + assert get_return_annotation(none_return) == "None" + + def test_custom_class_return_type(self): + class T: + pass + def class_return() -> T: + return T() + + assert get_return_annotation(class_return) == "T" + + def test_enum_return_type(self): + def enum_return() -> ExitCode: + return ExitCode(0) + + assert get_return_annotation(enum_return) == "ExitCode" + + def test_call_fixture_function_error(): """Check if an error is raised if a fixture function is called directly (#4545)""" @@ -5089,45 +5135,3 @@ def test_method(self, /, fix): ) result = pytester.runpytest() result.assert_outcomes(passed=1) - - -def test_get_return_annotation() -> None: - def six() -> int: - return 6 - - assert get_return_annotation(six) == "int" - - def two_sixes() -> tuple[int, str]: - return (6, "six") - - assert get_return_annotation(two_sixes) == "tuple[int, str]" - - def callable_return() -> Callable[..., Any]: - return two_sixes - - assert get_return_annotation(callable_return) == "Callable[..., Any]" - - def no_annotation(): - return 6 - - assert get_return_annotation(no_annotation) == "" - - def none_return() -> None: - pass - - assert get_return_annotation(none_return) == "None" - - class T: - pass - - def class_return() -> T: - return T() - - assert get_return_annotation(class_return) == "T" - - def enum_return() -> ExitCode: - return ExitCode(0) - - assert get_return_annotation(enum_return) == "ExitCode" - - assert get_return_annotation(range) == "" From 032a8748e1d95749c0f351e0d86a9f610bfdf366 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 19:01:12 +0000 Subject: [PATCH 10/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d5ca4196149..d2be3f80f2d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4618,15 +4618,16 @@ def none_return() -> None: def test_custom_class_return_type(self): class T: pass + def class_return() -> T: return T() - + assert get_return_annotation(class_return) == "T" def test_enum_return_type(self): def enum_return() -> ExitCode: return ExitCode(0) - + assert get_return_annotation(enum_return) == "ExitCode" From b78fe02479014bf2f4991a73879712f183188bbd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:16:48 +0000 Subject: [PATCH 11/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d2be3f80f2d..408c26ea19f 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,13 +1,13 @@ # mypy: allow-untyped-defs from __future__ import annotations +from collections.abc import Callable from itertools import zip_longest import os from pathlib import Path import sys import textwrap from typing import Any -from typing import Callable from _pytest.compat import getfuncargnames from _pytest.config import ExitCode From 3b6c1e95f29ec42ceee2a705baeb04060a9f33af Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 2 Oct 2025 11:44:41 +0330 Subject: [PATCH 12/18] Use regex approach to retrieve return annotations --- src/_pytest/fixtures.py | 22 +++----- testing/python/fixtures.py | 67 ------------------------ testing/python/show_fixtures_per_test.py | 24 --------- 3 files changed, 8 insertions(+), 105 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index eb4969e0ab3..0786600fd1f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -18,6 +18,7 @@ import inspect import os from pathlib import Path +import re import sys import types from typing import Any @@ -2016,20 +2017,13 @@ def _showfixtures_main(config: Config, session: Session) -> None: def get_return_annotation(fixture_func: Callable[..., Any]) -> str: - try: - sig = signature(fixture_func) - annotation = sig.return_annotation - if annotation is not sig.empty: - if type(annotation) == type(None): - return "None" - if isinstance(annotation, str): - return annotation - if annotation.__module__ == "typing": - return str(annotation).replace("typing.", "") - return str(annotation.__name__) - except (ValueError, TypeError): - pass - return "" + pattern = re.compile(r'\b(?:(?:<[^>]+>|[A-Za-z_]\w*)\.)+([A-Za-z_]\w*)\b') + + sig = str(signature(fixture_func)) + sig_parts = sig.split(" -> ") + if len(sig_parts) < 2: + return "" + return pattern.sub(r'\1', sig_parts[-1]) def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 408c26ea19f..dc7da2f3806 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,18 +1,15 @@ # mypy: allow-untyped-defs from __future__ import annotations -from collections.abc import Callable from itertools import zip_longest import os from pathlib import Path import sys import textwrap -from typing import Any from _pytest.compat import getfuncargnames from _pytest.config import ExitCode from _pytest.fixtures import deduplicate_names -from _pytest.fixtures import get_return_annotation from _pytest.fixtures import TopRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names @@ -3626,23 +3623,6 @@ def arg1(): ) result.stdout.no_fnmatch_line("*arg0*") - def test_show_fixtures_return_annotation(self, pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - @pytest.fixture - def six() -> int: - return 6 - """ - ) - result = pytester.runpytest("--fixtures", p) - result.stdout.fnmatch_lines( - """ - *fixtures defined from* - *six -> int -- test_show_fixtures_return_annotation.py:3* - """ - ) - @pytest.mark.parametrize("testmod", [True, False]) def test_show_fixtures_conftest(self, pytester: Pytester, testmod) -> None: pytester.makeconftest( @@ -4584,53 +4564,6 @@ def test_1(self, myfix): reprec.assertoutcome(passed=1) -class TestGetReturnAnnotation: - def test_primitive_return_type(self): - def six() -> int: - return 6 - - assert get_return_annotation(six) == "int" - - def test_compound_return_type(self): - def two_sixes() -> tuple[int, str]: - return (6, "six") - - assert get_return_annotation(two_sixes) == "tuple[int, str]" - - def test_callable_return_type(self): - def callable_return() -> Callable[..., Any]: - return self.test_compound_return_type - - assert get_return_annotation(callable_return) == "Callable[..., Any]" - - def test_no_annotation(self): - def no_annotation(): - return 6 - - assert get_return_annotation(no_annotation) == "" - - def test_none_return_type(self): - def none_return() -> None: - pass - - assert get_return_annotation(none_return) == "None" - - def test_custom_class_return_type(self): - class T: - pass - - def class_return() -> T: - return T() - - assert get_return_annotation(class_return) == "T" - - def test_enum_return_type(self): - def enum_return() -> ExitCode: - return ExitCode(0) - - assert get_return_annotation(enum_return) == "ExitCode" - - def test_call_fixture_function_error(): """Check if an error is raised if a fixture function is called directly (#4545)""" diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index 43289a37a7c..c860b61e21b 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -160,30 +160,6 @@ def test_args(arg2, arg3): ) -def test_show_return_annotation(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - @pytest.fixture - def five() -> int: - return 5 - def test_five(five): - pass - """ - ) - - result = pytester.runpytest("--fixtures-per-test", p) - assert result.ret == 0 - - result.stdout.fnmatch_lines( - [ - "*fixtures used by test_five*", - "*(test_show_return_annotation.py:6)*", - "five -> int -- test_show_return_annotation.py:3", - ] - ) - - def test_doctest_items(pytester: Pytester) -> None: pytester.makepyfile( ''' From 40d6a7cd7ad3f90ceddd8f0631151c510c6c999c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:15:09 +0000 Subject: [PATCH 13/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0786600fd1f..cf54869d718 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2017,13 +2017,13 @@ def _showfixtures_main(config: Config, session: Session) -> None: def get_return_annotation(fixture_func: Callable[..., Any]) -> str: - pattern = re.compile(r'\b(?:(?:<[^>]+>|[A-Za-z_]\w*)\.)+([A-Za-z_]\w*)\b') + pattern = re.compile(r"\b(?:(?:<[^>]+>|[A-Za-z_]\w*)\.)+([A-Za-z_]\w*)\b") sig = str(signature(fixture_func)) sig_parts = sig.split(" -> ") if len(sig_parts) < 2: return "" - return pattern.sub(r'\1', sig_parts[-1]) + return pattern.sub(r"\1", sig_parts[-1]) def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: From b3d9f1f6ee6f6987b73336d6350b979e6caa3b44 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 2 Oct 2025 11:55:28 +0330 Subject: [PATCH 14/18] Add separate test file for fixtures' return annotations --- testing/python/fixtures_return_annotation.py | 110 +++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 testing/python/fixtures_return_annotation.py diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py new file mode 100644 index 00000000000..1a3c61a3349 --- /dev/null +++ b/testing/python/fixtures_return_annotation.py @@ -0,0 +1,110 @@ +from collections.abc import Callable +from typing import Any + +from _pytest.config import ExitCode +from _pytest.fixtures import get_return_annotation +from _pytest.pytester import Pytester + + +class TestGetReturnAnnotation: + def test_primitive_return_type(self): + def six() -> int: + return 6 + + assert get_return_annotation(six) == "int" + + def test_compound_return_type(self): + def two_sixes() -> tuple[int, str]: + return (6, "six") + + assert get_return_annotation(two_sixes) == "tuple[int, str]" + + def test_callable_return_type(self): + def callable_return() -> Callable[..., Any]: + return self.test_compound_return_type + + assert get_return_annotation(callable_return) == "Callable[..., Any]" + + def test_no_annotation(self): + def no_annotation(): + return 6 + + assert get_return_annotation(no_annotation) == "" + + def test_none_return_type(self): + def none_return() -> None: + pass + + assert get_return_annotation(none_return) == "None" + + def test_custom_class_return_type(self): + class T: + pass + + def class_return() -> T: + return T() + + assert get_return_annotation(class_return) == "T" + + def test_enum_return_type(self): + def enum_return() -> ExitCode: + return ExitCode(0) + + assert get_return_annotation(enum_return) == "ExitCode" + + def test_with_arg_annotations(self): + def with_args(a: Callable[[], None], b: list) -> range: + return range(2) + + assert get_return_annotation(with_args) == "range" + + def test_invalid_return_type(self): + def bad_annotation() -> 6: # type: ignore + return 6 + + assert get_return_annotation(bad_annotation) == "6" + + def test_unobtainable_signature(self): + assert get_return_annotation(len) == "" + + +def test_fixtures_return_annotation(pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + @pytest.fixture + def six() -> int: + return 6 + """ + ) + result = pytester.runpytest("--fixtures", p) + result.stdout.fnmatch_lines( + """ + *fixtures defined from* + *six -> int -- test_fixtures_return_annotation.py:3* + """ + ) + + +def test_fixtures_per_test_return_annotation(pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + @pytest.fixture + def five() -> int: + return 5 + def test_five(five): + pass + """ + ) + + result = pytester.runpytest("--fixtures-per-test", p) + assert result.ret == 0 + + result.stdout.fnmatch_lines( + [ + "*fixtures used by test_five*", + "*(test_fixtures_per_test_return_annotation.py:6)*", + "five -> int -- test_fixtures_per_test_return_annotation.py:3", + ] + ) From 1c18e591c60923ba25ec5312f6fa1f038c49633e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:26:04 +0000 Subject: [PATCH 15/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures_return_annotation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py index 1a3c61a3349..66e76a0f6d8 100644 --- a/testing/python/fixtures_return_annotation.py +++ b/testing/python/fixtures_return_annotation.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable from typing import Any @@ -59,7 +61,7 @@ def with_args(a: Callable[[], None], b: list) -> range: assert get_return_annotation(with_args) == "range" def test_invalid_return_type(self): - def bad_annotation() -> 6: # type: ignore + def bad_annotation() -> 6: # type: ignore return 6 assert get_return_annotation(bad_annotation) == "6" From 4d88e6b27083d97fd7b9287da48b91d597f5a093 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 2 Oct 2025 11:59:51 +0330 Subject: [PATCH 16/18] Silence mypy errors in 'fixtures_return_annotation.py' --- testing/python/fixtures_return_annotation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py index 66e76a0f6d8..78c7aed98c8 100644 --- a/testing/python/fixtures_return_annotation.py +++ b/testing/python/fixtures_return_annotation.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs from __future__ import annotations from collections.abc import Callable @@ -55,7 +56,7 @@ def enum_return() -> ExitCode: assert get_return_annotation(enum_return) == "ExitCode" def test_with_arg_annotations(self): - def with_args(a: Callable[[], None], b: list) -> range: + def with_args(a: Callable[[], None], b: str) -> range: return range(2) assert get_return_annotation(with_args) == "range" From becaff7e33ddff9e3ca6cde521d1e3ad80b277cd Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 2 Oct 2025 15:31:33 +0330 Subject: [PATCH 17/18] Use formatannotation with regex --- src/_pytest/fixtures.py | 10 ++++++---- testing/python/fixtures_return_annotation.py | 9 +++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cf54869d718..1ddc0da7deb 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2019,11 +2019,13 @@ def _showfixtures_main(config: Config, session: Session) -> None: def get_return_annotation(fixture_func: Callable[..., Any]) -> str: pattern = re.compile(r"\b(?:(?:<[^>]+>|[A-Za-z_]\w*)\.)+([A-Za-z_]\w*)\b") - sig = str(signature(fixture_func)) - sig_parts = sig.split(" -> ") - if len(sig_parts) < 2: + sig = signature(fixture_func) + return_annotation = sig.return_annotation + if return_annotation is sig.empty: return "" - return pattern.sub(r"\1", sig_parts[-1]) + if not isinstance(return_annotation, str): + return_annotation = inspect.formatannotation(return_annotation) + return pattern.sub(r"\1", return_annotation) def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py index 78c7aed98c8..d99a4dc3825 100644 --- a/testing/python/fixtures_return_annotation.py +++ b/testing/python/fixtures_return_annotation.py @@ -1,6 +1,5 @@ # mypy: allow-untyped-defs -from __future__ import annotations - +# ruff: noqa: FA100 from collections.abc import Callable from typing import Any @@ -61,6 +60,12 @@ def with_args(a: Callable[[], None], b: str) -> range: assert get_return_annotation(with_args) == "range" + def test_string_return_annotation(self): + def string_return_annotation() -> "int": + return 6 + + assert get_return_annotation(string_return_annotation) == "int" + def test_invalid_return_type(self): def bad_annotation() -> 6: # type: ignore return 6 From f555682bb33f37729782a26dd35d0e7fc7841342 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:02:05 +0000 Subject: [PATCH 18/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/python/fixtures_return_annotation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py index d99a4dc3825..8f79297402c 100644 --- a/testing/python/fixtures_return_annotation.py +++ b/testing/python/fixtures_return_annotation.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs -# ruff: noqa: FA100 +from __future__ import annotations + from collections.abc import Callable from typing import Any @@ -61,7 +62,7 @@ def with_args(a: Callable[[], None], b: str) -> range: assert get_return_annotation(with_args) == "range" def test_string_return_annotation(self): - def string_return_annotation() -> "int": + def string_return_annotation() -> int: return 6 assert get_return_annotation(string_return_annotation) == "int"