This commit is contained in:
Iliyan Angelov
2025-12-06 03:27:35 +02:00
parent 7667eb5eda
commit 5a8ca3c475
2211 changed files with 28086 additions and 37066 deletions

View File

@@ -1,58 +1,44 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration."""
from __future__ import annotations
import argparse
from collections.abc import Generator
from collections.abc import Sequence
import os
import sys
from typing import Any
from argparse import Action
from typing import List
from typing import Optional
from typing import Union
import pytest
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
import pytest
class HelpAction(argparse.Action):
"""An argparse Action that will raise a PrintHelp exception in order to skip
the rest of the argument parsing when --help is passed.
class HelpAction(Action):
"""An argparse Action that will raise an exception in order to skip the
rest of the argument parsing when --help is passed.
This prevents argparse from raising UsageError when `--help` is used along
with missing required arguments when any are defined, for example by
``pytest_addoption``. This is similar to the way that the builtin argparse
--help option is implemented by raising SystemExit.
To opt in to this behavior, the parse caller must set
`namespace._raise_print_help = True`. Otherwise it just sets the option.
This prevents argparse from quitting due to missing required arguments
when any are defined, for example by ``pytest_addoption``.
This is similar to the way that the builtin argparse --help option is
implemented by raising SystemExit.
"""
def __init__(
self, option_strings: Sequence[str], dest: str, *, help: str | None = None
) -> None:
def __init__(self, option_strings, dest=None, default=False, help=None):
super().__init__(
option_strings=option_strings,
dest=dest,
nargs=0,
const=True,
default=False,
default=default,
nargs=0,
help=help,
)
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, self.const)
if getattr(namespace, "_raise_print_help", False):
# We should only skip the rest of the parsing after preparse is done.
if getattr(parser._parser, "after_preparse", False):
raise PrintHelp
@@ -67,14 +53,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Display pytest version and information about plugins. "
"When given twice, also display information about plugins.",
)
group._addoption( # private to use reserved lower-case short option
group._addoption(
"-h",
"--help",
action=HelpAction,
dest="help",
help="Show help message and configuration info",
)
group._addoption( # private to use reserved lower-case short option
group._addoption(
"-p",
action="append",
dest="plugins",
@@ -82,14 +68,7 @@ def pytest_addoption(parser: Parser) -> None:
metavar="name",
help="Early-load given plugin module name or entry point (multi-allowed). "
"To avoid loading of plugins, use the `no:` prefix, e.g. "
"`no:doctest`. See also --disable-plugin-autoload.",
)
group.addoption(
"--disable-plugin-autoload",
action="store_true",
default=False,
help="Disable plugin auto-loading through entry point packaging metadata. "
"Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.",
"`no:doctest`.",
)
group.addoption(
"--traceconfig",
@@ -109,78 +88,79 @@ def pytest_addoption(parser: Parser) -> None:
"This file is opened with 'w' and truncated as a result, care advised. "
"Default: pytestdebug.log.",
)
group._addoption( # private to use reserved lower-case short option
group._addoption(
"-o",
"--override-ini",
dest="override_ini",
action="append",
help='Override configuration option with "option=value" style, '
"e.g. `-o strict_xfail=True -o cache_dir=cache`.",
help='Override ini option with "option=value" style, '
"e.g. `-o xfail_strict=True -o cache_dir=cache`.",
)
@pytest.hookimpl(wrapper=True)
def pytest_cmdline_parse() -> Generator[None, Config, Config]:
config = yield
@pytest.hookimpl(hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config: Config = outcome.get_result()
if config.option.debug:
# --debug | --debug <file.log> was provided.
path = config.option.debug
debugfile = open(path, "w", encoding="utf-8")
debugfile.write(
"versions pytest-{}, "
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
"versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n"
% (
pytest.__version__,
".".join(map(str, sys.version_info)),
config.invocation_params.dir,
os.getcwd(),
config.invocation_params.args,
)
)
config.trace.root.setwriter(debugfile.write)
undo_tracing = config.pluginmanager.enable_tracing()
sys.stderr.write(f"writing pytest debug information to {path}\n")
sys.stderr.write("writing pytest debug information to %s\n" % path)
def unset_tracing() -> None:
debugfile.close()
sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n")
sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)
config.trace.root.setwriter(None)
undo_tracing()
config.add_cleanup(unset_tracing)
return config
def show_version_verbose(config: Config) -> None:
"""Show verbose pytest version installation, including plugins."""
sys.stdout.write(
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stdout.write(line + "\n")
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
# Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire
# pytest infrastructure just to display the version (#13574).
def showversion(config: Config) -> None:
if config.option.version > 1:
show_version_verbose(config)
return ExitCode.OK
sys.stdout.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stdout.write(line + "\n")
else:
sys.stdout.write(f"pytest {pytest.__version__}\n")
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.version > 0:
showversion(config)
return 0
elif config.option.help:
config._do_configure()
showhelp(config)
config._ensure_unconfigure()
return ExitCode.OK
return 0
return None
def showhelp(config: Config) -> None:
import textwrap
reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
@@ -188,20 +168,22 @@ def showhelp(config: Config) -> None:
tw.write(config._parser.optparser.format_help())
tw.line()
tw.line(
"[pytest] configuration options in the first "
"pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
"[pytest] ini-options in the first "
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
)
tw.line()
columns = tw.fullwidth # costly call
indent_len = 24 # based on argparse's max_help_position=24
indent = " " * indent_len
for name in config._parser._inidict:
help, type, _default = config._parser._inidict[name]
for name in config._parser._ininames:
help, type, default = config._parser._inidict[name]
if type is None:
type = "string"
if help is None:
raise TypeError(f"help argument cannot be None for {name}")
spec = f"{name} ({type}):"
tw.write(f" {spec}")
tw.write(" %s" % spec)
spec_len = len(spec)
if spec_len > (indent_len - 3):
# Display help starting at a new line.
@@ -229,19 +211,10 @@ def showhelp(config: Config) -> None:
tw.line()
tw.line("Environment variables:")
vars = [
(
"CI",
"When set to a non-empty value, pytest knows it is running in a "
"CI process and does not truncate summary info",
),
("BUILD_NUMBER", "Equivalent to CI"),
("PYTEST_ADDOPTS", "Extra command line options"),
("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"),
("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"),
("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"),
("PYTEST_DEBUG_TEMPROOT", "Override the system temporary directory"),
("PYTEST_THEME", "The Pygments style to use for code output"),
("PYTEST_THEME_MODE", "Set the PYTEST_THEME to be either 'dark' or 'light'"),
]
for name, help in vars:
tw.line(f" {name:<24} {help}")
@@ -258,13 +231,17 @@ def showhelp(config: Config) -> None:
for warningreport in reporter.stats.get("warnings", []):
tw.line("warning : " + warningreport.message, red=True)
return
def getpluginversioninfo(config: Config) -> list[str]:
conftest_options = [("pytest_plugins", "list of plugin names to load")]
def getpluginversioninfo(config: Config) -> List[str]:
lines = []
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
lines.append("registered third-party plugins:")
lines.append("setuptools registered plugins:")
for plugin, dist in plugininfo:
loc = getattr(plugin, "__file__", repr(plugin))
content = f"{dist.project_name}-{dist.version} at {loc}"
@@ -272,7 +249,7 @@ def getpluginversioninfo(config: Config) -> list[str]:
return lines
def pytest_report_header(config: Config) -> list[str]:
def pytest_report_header(config: Config) -> List[str]:
lines = []
if config.option.debug or config.option.traceconfig:
lines.append(f"using: pytest-{pytest.__version__}")