helpconfig.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # mypy: allow-untyped-defs
  2. """Version info, help messages, tracing configuration."""
  3. from __future__ import annotations
  4. import argparse
  5. from collections.abc import Generator
  6. from collections.abc import Sequence
  7. import os
  8. import sys
  9. from typing import Any
  10. from _pytest.config import Config
  11. from _pytest.config import ExitCode
  12. from _pytest.config import PrintHelp
  13. from _pytest.config.argparsing import Parser
  14. from _pytest.terminal import TerminalReporter
  15. import pytest
  16. class HelpAction(argparse.Action):
  17. """An argparse Action that will raise a PrintHelp exception in order to skip
  18. the rest of the argument parsing when --help is passed.
  19. This prevents argparse from raising UsageError when `--help` is used along
  20. with missing required arguments when any are defined, for example by
  21. ``pytest_addoption``. This is similar to the way that the builtin argparse
  22. --help option is implemented by raising SystemExit.
  23. To opt in to this behavior, the parse caller must set
  24. `namespace._raise_print_help = True`. Otherwise it just sets the option.
  25. """
  26. def __init__(
  27. self, option_strings: Sequence[str], dest: str, *, help: str | None = None
  28. ) -> None:
  29. super().__init__(
  30. option_strings=option_strings,
  31. dest=dest,
  32. nargs=0,
  33. const=True,
  34. default=False,
  35. help=help,
  36. )
  37. def __call__(
  38. self,
  39. parser: argparse.ArgumentParser,
  40. namespace: argparse.Namespace,
  41. values: str | Sequence[Any] | None,
  42. option_string: str | None = None,
  43. ) -> None:
  44. setattr(namespace, self.dest, self.const)
  45. if getattr(namespace, "_raise_print_help", False):
  46. raise PrintHelp
  47. def pytest_addoption(parser: Parser) -> None:
  48. group = parser.getgroup("debugconfig")
  49. group.addoption(
  50. "--version",
  51. "-V",
  52. action="count",
  53. default=0,
  54. dest="version",
  55. help="Display pytest version and information about plugins. "
  56. "When given twice, also display information about plugins.",
  57. )
  58. group._addoption( # private to use reserved lower-case short option
  59. "-h",
  60. "--help",
  61. action=HelpAction,
  62. dest="help",
  63. help="Show help message and configuration info",
  64. )
  65. group._addoption( # private to use reserved lower-case short option
  66. "-p",
  67. action="append",
  68. dest="plugins",
  69. default=[],
  70. metavar="name",
  71. help="Early-load given plugin module name or entry point (multi-allowed). "
  72. "To avoid loading of plugins, use the `no:` prefix, e.g. "
  73. "`no:doctest`. See also --disable-plugin-autoload.",
  74. )
  75. group.addoption(
  76. "--disable-plugin-autoload",
  77. action="store_true",
  78. default=False,
  79. help="Disable plugin auto-loading through entry point packaging metadata. "
  80. "Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.",
  81. )
  82. group.addoption(
  83. "--traceconfig",
  84. "--trace-config",
  85. action="store_true",
  86. default=False,
  87. help="Trace considerations of conftest.py files",
  88. )
  89. group.addoption(
  90. "--debug",
  91. action="store",
  92. nargs="?",
  93. const="pytestdebug.log",
  94. dest="debug",
  95. metavar="DEBUG_FILE_NAME",
  96. help="Store internal tracing debug information in this log file. "
  97. "This file is opened with 'w' and truncated as a result, care advised. "
  98. "Default: pytestdebug.log.",
  99. )
  100. group._addoption( # private to use reserved lower-case short option
  101. "-o",
  102. "--override-ini",
  103. dest="override_ini",
  104. action="append",
  105. help='Override configuration option with "option=value" style, '
  106. "e.g. `-o strict_xfail=True -o cache_dir=cache`.",
  107. )
  108. @pytest.hookimpl(wrapper=True)
  109. def pytest_cmdline_parse() -> Generator[None, Config, Config]:
  110. config = yield
  111. if config.option.debug:
  112. # --debug | --debug <file.log> was provided.
  113. path = config.option.debug
  114. debugfile = open(path, "w", encoding="utf-8")
  115. debugfile.write(
  116. "versions pytest-{}, "
  117. "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
  118. pytest.__version__,
  119. ".".join(map(str, sys.version_info)),
  120. config.invocation_params.dir,
  121. os.getcwd(),
  122. config.invocation_params.args,
  123. )
  124. )
  125. config.trace.root.setwriter(debugfile.write)
  126. undo_tracing = config.pluginmanager.enable_tracing()
  127. sys.stderr.write(f"writing pytest debug information to {path}\n")
  128. def unset_tracing() -> None:
  129. debugfile.close()
  130. sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n")
  131. config.trace.root.setwriter(None)
  132. undo_tracing()
  133. config.add_cleanup(unset_tracing)
  134. return config
  135. def show_version_verbose(config: Config) -> None:
  136. """Show verbose pytest version installation, including plugins."""
  137. sys.stdout.write(
  138. f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
  139. )
  140. plugininfo = getpluginversioninfo(config)
  141. if plugininfo:
  142. for line in plugininfo:
  143. sys.stdout.write(line + "\n")
  144. def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
  145. # Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire
  146. # pytest infrastructure just to display the version (#13574).
  147. if config.option.version > 1:
  148. show_version_verbose(config)
  149. return ExitCode.OK
  150. elif config.option.help:
  151. config._do_configure()
  152. showhelp(config)
  153. config._ensure_unconfigure()
  154. return ExitCode.OK
  155. return None
  156. def showhelp(config: Config) -> None:
  157. import textwrap
  158. reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
  159. "terminalreporter"
  160. )
  161. assert reporter is not None
  162. tw = reporter._tw
  163. tw.write(config._parser.optparser.format_help())
  164. tw.line()
  165. tw.line(
  166. "[pytest] configuration options in the first "
  167. "pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
  168. )
  169. tw.line()
  170. columns = tw.fullwidth # costly call
  171. indent_len = 24 # based on argparse's max_help_position=24
  172. indent = " " * indent_len
  173. for name in config._parser._inidict:
  174. help, type, _default = config._parser._inidict[name]
  175. if help is None:
  176. raise TypeError(f"help argument cannot be None for {name}")
  177. spec = f"{name} ({type}):"
  178. tw.write(f" {spec}")
  179. spec_len = len(spec)
  180. if spec_len > (indent_len - 3):
  181. # Display help starting at a new line.
  182. tw.line()
  183. helplines = textwrap.wrap(
  184. help,
  185. columns,
  186. initial_indent=indent,
  187. subsequent_indent=indent,
  188. break_on_hyphens=False,
  189. )
  190. for line in helplines:
  191. tw.line(line)
  192. else:
  193. # Display help starting after the spec, following lines indented.
  194. tw.write(" " * (indent_len - spec_len - 2))
  195. wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)
  196. if wrapped:
  197. tw.line(wrapped[0])
  198. for line in wrapped[1:]:
  199. tw.line(indent + line)
  200. tw.line()
  201. tw.line("Environment variables:")
  202. vars = [
  203. (
  204. "CI",
  205. "When set to a non-empty value, pytest knows it is running in a "
  206. "CI process and does not truncate summary info",
  207. ),
  208. ("BUILD_NUMBER", "Equivalent to CI"),
  209. ("PYTEST_ADDOPTS", "Extra command line options"),
  210. ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"),
  211. ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"),
  212. ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"),
  213. ("PYTEST_DEBUG_TEMPROOT", "Override the system temporary directory"),
  214. ("PYTEST_THEME", "The Pygments style to use for code output"),
  215. ("PYTEST_THEME_MODE", "Set the PYTEST_THEME to be either 'dark' or 'light'"),
  216. ]
  217. for name, help in vars:
  218. tw.line(f" {name:<24} {help}")
  219. tw.line()
  220. tw.line()
  221. tw.line("to see available markers type: pytest --markers")
  222. tw.line("to see available fixtures type: pytest --fixtures")
  223. tw.line(
  224. "(shown according to specified file_or_dir or current dir "
  225. "if not specified; fixtures with leading '_' are only shown "
  226. "with the '-v' option"
  227. )
  228. for warningreport in reporter.stats.get("warnings", []):
  229. tw.line("warning : " + warningreport.message, red=True)
  230. def getpluginversioninfo(config: Config) -> list[str]:
  231. lines = []
  232. plugininfo = config.pluginmanager.list_plugin_distinfo()
  233. if plugininfo:
  234. lines.append("registered third-party plugins:")
  235. for plugin, dist in plugininfo:
  236. loc = getattr(plugin, "__file__", repr(plugin))
  237. content = f"{dist.project_name}-{dist.version} at {loc}"
  238. lines.append(" " + content)
  239. return lines
  240. def pytest_report_header(config: Config) -> list[str]:
  241. lines = []
  242. if config.option.debug or config.option.traceconfig:
  243. lines.append(f"using: pytest-{pytest.__version__}")
  244. verinfo = getpluginversioninfo(config)
  245. if verinfo:
  246. lines.extend(verinfo)
  247. if config.option.traceconfig:
  248. lines.append("active plugins:")
  249. items = config.pluginmanager.list_name_plugin()
  250. for name, plugin in items:
  251. if hasattr(plugin, "__file__"):
  252. r = plugin.__file__
  253. else:
  254. r = repr(plugin)
  255. lines.append(f" {name:<20}: {r}")
  256. return lines