From 3687117d32042807b72001dd638c01c4e112ebec Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 21:22:09 +0330 Subject: [PATCH 01/10] 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 bc5805aaea9..c77885c0c9f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1915,6 +1915,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) @@ -1999,6 +2002,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) @@ -2013,6 +2019,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 fb76fe6cf96..596f70741d3 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3581,9 +3581,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*", ] ) @@ -3592,9 +3592,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*", ] ) @@ -3614,7 +3614,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* @@ -3622,6 +3622,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 33875322e8088ae43510e68494c3881fa7a6edcd Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 21:27:05 +0330 Subject: [PATCH 02/10] 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 ce68932062e76870739c0297a356aa9ba7b53223 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/10] [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 596f70741d3..91abfc80a56 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3624,12 +3624,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 536c8d27fec6f6db4516fa5b0219559b6b0e722a Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 22:05:15 +0330 Subject: [PATCH 04/10] 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 c77885c0c9f..abad490088c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1915,7 +1915,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) @@ -2002,7 +2002,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": @@ -2019,12 +2019,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 91abfc80a56..9247870b1ab 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5,10 +5,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 @@ -3634,7 +3636,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* """ @@ -5086,3 +5087,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 c3e0a48f8239645dd5941765acc314abc4bc296e 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/10] [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 9247870b1ab..a5b436c209c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5,7 +5,6 @@ from pathlib import Path import sys import textwrap -from typing import Tuple from _pytest.compat import getfuncargnames from _pytest.config import ExitCode @@ -5088,19 +5087,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 ebe8c9c919f0c2ecfc11a89a5fb93d069453377b Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 27 Aug 2025 22:08:08 +0330 Subject: [PATCH 06/10] 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 abad490088c..c3b4cc6d49a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2019,7 +2019,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 a5b436c209c..c8e5ce4f1fc 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5097,7 +5097,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 @@ -5108,3 +5108,5 @@ def none_return() -> None: pass assert get_return_annotation(none_return) == "None" + + assert get_return_annotation(range) == "" From 79ce6825fcb68343dde8d8a422877556632f539f Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 28 Aug 2025 12:11:58 +0330 Subject: [PATCH 07/10] 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 c3b4cc6d49a..00969d05ee4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2023,8 +2023,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 c8e5ce4f1fc..b05fc51a1d5 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5,6 +5,7 @@ from pathlib import Path import sys import textwrap +from typing import Any, Callable from _pytest.compat import getfuncargnames from _pytest.config import ExitCode @@ -5099,14 +5100,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 eb16715718da61b1c784d7a95a9a10a1b1688023 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/10] [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 b05fc51a1d5..26eb10ecd4b 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5,7 +5,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 @@ -5117,14 +5118,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 776560e961a8c7a388c10302743dedb842c18e0d Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Thu, 28 Aug 2025 22:30:40 +0330 Subject: [PATCH 09/10] 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 00969d05ee4..e386f5343b1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2024,11 +2024,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 26eb10ecd4b..0f87818d279 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4583,6 +4583,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)""" @@ -5088,45 +5134,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 b7c7827e77acda563f74be133cd5328ad28fd01e 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/10] [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 0f87818d279..b6958568560 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4617,15 +4617,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"