Move show-fixtures code from python.py to fixtures.py

It makes more sense, also, we have a long term idea of generalizing
fixture support to items defined by other plugins, not just python, in
which case `--fixtures` would definitely not be python-plugin
specific.
This commit is contained in:
Ran Benita 2024-02-02 15:02:48 +02:00
parent aa5324560b
commit 1188ea2608
2 changed files with 162 additions and 165 deletions

View File

@ -51,6 +51,7 @@ from _pytest.compat import NotSetType
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
@ -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]:
@ -1761,3 +1789,137 @@ class FixtureManager:
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)

View File

@ -40,13 +40,11 @@ from _pytest._code import getfslineno
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
@ -54,7 +52,6 @@ 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
@ -71,7 +68,6 @@ from _pytest.mark.structures import MarkDecorator
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
@ -88,27 +84,7 @@ if TYPE_CHECKING:
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",
@ -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)
@ -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.