diff --git a/changelog/13676.improvement.rst b/changelog/13676.improvement.rst new file mode 100644 index 00000000000..fc79a1a6de0 --- /dev/null +++ b/changelog/13676.improvement.rst @@ -0,0 +1 @@ +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 91f1b3a67f6..1ddc0da7deb 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 @@ -1911,6 +1912,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.func) + 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 +1999,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.func) + 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 +2016,18 @@ def _showfixtures_main(config: Config, session: Session) -> None: tw.line() +def get_return_annotation(fixture_func: Callable[..., Any]) -> str: + pattern = re.compile(r"\b(?:(?:<[^>]+>|[A-Za-z_]\w*)\.)+([A-Za-z_]\w*)\b") + + sig = signature(fixture_func) + return_annotation = sig.return_annotation + if return_annotation is sig.empty: + return "" + 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: for line in doc.split("\n"): tw.line(indent + line) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8b97d35c21e..dc7da2f3806 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* diff --git a/testing/python/fixtures_return_annotation.py b/testing/python/fixtures_return_annotation.py new file mode 100644 index 00000000000..8f79297402c --- /dev/null +++ b/testing/python/fixtures_return_annotation.py @@ -0,0 +1,119 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +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: str) -> range: + return range(2) + + 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 + + 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", + ] + )