helpconfig.py 9.2 KB

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