Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
162 changes: 162 additions & 0 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
Expand Down Expand Up @@ -1365,6 +1366,33 @@ def pytest_addoption(parser: Parser) -> None:
default=[],
help="List of default fixtures to be used with this project",
)
group = parser.getgroup("general")
group.addoption(
"--fixtures",
"--funcargs",
action="store_true",
dest="showfixtures",
default=False,
help="Show available fixtures, sorted by plugin appearance "
"(fixtures with leading '_' are only shown with '-v')",
)
group.addoption(
"--fixtures-per-test",
action="store_true",
dest="show_fixtures_per_test",
default=False,
help="Show fixtures per test",
)


def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.showfixtures:
showfixtures(config)
return 0
if config.option.show_fixtures_per_test:
show_fixtures_per_test(config)
return 0
return None


def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
Expand Down Expand Up @@ -1761,3 +1789,137 @@ def _matchfactories(
for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids:
yield fixturedef


def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
from _pytest.main import wrap_session

return wrap_session(config, _show_fixtures_per_test)


_PYTEST_DIR = Path(_pytest.__file__).parent


def _pretty_fixture_path(invocation_dir: Path, func) -> str:
loc = Path(getlocation(func, invocation_dir))
prefix = Path("...", "_pytest")
try:
return str(prefix / loc.relative_to(_PYTEST_DIR))
except ValueError:
return bestrelpath(invocation_dir, loc)


def _show_fixtures_per_test(config: Config, session: "Session") -> None:
import _pytest.config

session.perform_collect()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")

def get_best_relpath(func) -> str:
loc = getlocation(func, invocation_dir)
return bestrelpath(invocation_dir, Path(loc))

def write_fixture(fixture_def: FixtureDef[object]) -> None:
argname = fixture_def.argname
if verbose <= 0 and argname.startswith("_"):
return
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
tw.write(f"{argname}", green=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
fixture_doc = inspect.getdoc(fixture_def.func)
if fixture_doc:
write_docstring(
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
)
else:
tw.line(" no docstring available", red=True)

def write_item(item: nodes.Item) -> None:
# Not all items have _fixtureinfo attribute.
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
if info is None or not info.name2fixturedefs:
# This test item does not use any fixtures.
return
tw.line()
tw.sep("-", f"fixtures used by {item.name}")
# TODO: Fix this type ignore.
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
# dict key not used in loop but needed for sorting.
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None
if not fixturedefs:
continue
# Last item is expected to be the one used by the test item.
write_fixture(fixturedefs[-1])

for session_item in session.items:
write_item(session_item)


def showfixtures(config: Config) -> Union[int, ExitCode]:
from _pytest.main import wrap_session

return wrap_session(config, _showfixtures_main)


def _showfixtures_main(config: Config, session: "Session") -> None:
import _pytest.config

session.perform_collect()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")

fm = session._fixturemanager

available = []
seen: Set[Tuple[str, str]] = set()

for argname, fixturedefs in fm._arg2fixturedefs.items():
assert fixturedefs is not None
if not fixturedefs:
continue
for fixturedef in fixturedefs:
loc = getlocation(fixturedef.func, invocation_dir)
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
available.append(
(
len(fixturedef.baseid),
fixturedef.func.__module__,
_pretty_fixture_path(invocation_dir, fixturedef.func),
fixturedef.argname,
fixturedef,
)
)

available.sort()
currentmodule = None
for baseid, module, prettypath, argname, fixturedef in available:
if currentmodule != module:
if not module.startswith("_pytest."):
tw.line()
tw.sep("-", f"fixtures defined from {module}")
currentmodule = module
if verbose <= 0 and argname.startswith("_"):
continue
tw.write(f"{argname}", green=True)
if fixturedef.scope != "function":
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
doc = inspect.getdoc(fixturedef.func)
if doc:
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
else:
tw.line(" no docstring available", red=True)
tw.line()


def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
for line in doc.split("\n"):
tw.line(indent + line)
165 changes: 0 additions & 165 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,18 @@
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_async_function
from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
Expand All @@ -71,7 +68,6 @@
from _pytest.mark.structures import normalize_mark_list
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
Expand All @@ -88,27 +84,7 @@
from typing import Self


_PYTEST_DIR = Path(_pytest.__file__).parent


def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group.addoption(
"--fixtures",
"--funcargs",
action="store_true",
dest="showfixtures",
default=False,
help="Show available fixtures, sorted by plugin appearance "
"(fixtures with leading '_' are only shown with '-v')",
)
group.addoption(
"--fixtures-per-test",
action="store_true",
dest="show_fixtures_per_test",
default=False,
help="Show fixtures per test",
)
parser.addini(
"python_files",
type="args",
Expand Down Expand Up @@ -137,16 +113,6 @@ def pytest_addoption(parser: Parser) -> None:
)


def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.showfixtures:
showfixtures(config)
return 0
if config.option.show_fixtures_per_test:
show_fixtures_per_test(config)
return 0
return None


def pytest_generate_tests(metafunc: "Metafunc") -> None:
for marker in metafunc.definition.iter_markers(name="parametrize"):
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
Expand Down Expand Up @@ -1525,137 +1491,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
return val if escape_option else ascii_escaped(val) # type: ignore


def _pretty_fixture_path(invocation_dir: Path, func) -> str:
loc = Path(getlocation(func, invocation_dir))
prefix = Path("...", "_pytest")
try:
return str(prefix / loc.relative_to(_PYTEST_DIR))
except ValueError:
return bestrelpath(invocation_dir, loc)


def show_fixtures_per_test(config):
from _pytest.main import wrap_session

return wrap_session(config, _show_fixtures_per_test)


def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config

session.perform_collect()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")

def get_best_relpath(func) -> str:
loc = getlocation(func, invocation_dir)
return bestrelpath(invocation_dir, Path(loc))

def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
argname = fixture_def.argname
if verbose <= 0 and argname.startswith("_"):
return
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
tw.write(f"{argname}", green=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
fixture_doc = inspect.getdoc(fixture_def.func)
if fixture_doc:
write_docstring(
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
)
else:
tw.line(" no docstring available", red=True)

def write_item(item: nodes.Item) -> None:
# Not all items have _fixtureinfo attribute.
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
if info is None or not info.name2fixturedefs:
# This test item does not use any fixtures.
return
tw.line()
tw.sep("-", f"fixtures used by {item.name}")
# TODO: Fix this type ignore.
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
# dict key not used in loop but needed for sorting.
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None
if not fixturedefs:
continue
# Last item is expected to be the one used by the test item.
write_fixture(fixturedefs[-1])

for session_item in session.items:
write_item(session_item)


def showfixtures(config: Config) -> Union[int, ExitCode]:
from _pytest.main import wrap_session

return wrap_session(config, _showfixtures_main)


def _showfixtures_main(config: Config, session: Session) -> None:
import _pytest.config

session.perform_collect()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")

fm = session._fixturemanager

available = []
seen: Set[Tuple[str, str]] = set()

for argname, fixturedefs in fm._arg2fixturedefs.items():
assert fixturedefs is not None
if not fixturedefs:
continue
for fixturedef in fixturedefs:
loc = getlocation(fixturedef.func, invocation_dir)
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
available.append(
(
len(fixturedef.baseid),
fixturedef.func.__module__,
_pretty_fixture_path(invocation_dir, fixturedef.func),
fixturedef.argname,
fixturedef,
)
)

available.sort()
currentmodule = None
for baseid, module, prettypath, argname, fixturedef in available:
if currentmodule != module:
if not module.startswith("_pytest."):
tw.line()
tw.sep("-", f"fixtures defined from {module}")
currentmodule = module
if verbose <= 0 and argname.startswith("_"):
continue
tw.write(f"{argname}", green=True)
if fixturedef.scope != "function":
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
doc = inspect.getdoc(fixturedef.func)
if doc:
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
else:
tw.line(" no docstring available", red=True)
tw.line()


def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
for line in doc.split("\n"):
tw.line(indent + line)


class Function(PyobjMixin, nodes.Item):
"""Item responsible for setting up and executing a Python test function.

Expand Down