Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/13676.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return type annotations are now shown in ``--fixtures`` and ``--fixtures-per-test``.
19 changes: 19 additions & 0 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import inspect
import os
from pathlib import Path
import re
import sys
import types
from typing import Any
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
10 changes: 5 additions & 5 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*",
]
)
Expand All @@ -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*",
]
)
Expand All @@ -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*
Expand Down
119 changes: 119 additions & 0 deletions testing/python/fixtures_return_annotation.py
Original file line number Diff line number Diff line change
@@ -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",
]
)