1"""Version info, help messages, tracing configuration."""

2 import os

3 import sys

4 from argparse import Action

5 from typing import List

6 from typing import Optional

7 from typing import Union

8

9 import pytest

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

15

16 class HelpAction(Action):

17 """An argparse Action that will raise an exception in order to skip the

18 rest of the argument parsing when --help is passed.

19

20 This prevents argparse from quitting due to missing required arguments

21 when any are defined, for example by ``pytest_addoption``.

22 This is similar to the way that the builtin argparse --help option is

23 implemented by raising SystemExit.

24 """

25

26 def __init__(self, option_strings, dest=None, default=False, help=None):

27 super().__init__(

28 option_strings=option_strings,

29 dest=dest,

30 const=True,

31 default=default,

32 nargs=0,

33 help=help,

34 )

35

36 def __call__(self, parser, namespace, values, option_string=None):

37 setattr(namespace, self.dest, self.const)

38

39 # We should only skip the rest of the parsing after preparse is done.

40 if getattr(parser._parser, "after_preparse", False):

41 raise PrintHelp

42

43

44 def pytest_addoption(parser: Parser) -> None:

45 group = parser.getgroup("debugconfig")

46 group.addoption(

47 "--version",

48 "-V",

49 action="count",

50 default=0,

51 dest="version",

52 help="Display pytest version and information about plugins. "

53 "When given twice, also display information about plugins.",

54 )

55 group._addoption(

56 "-h",

57 "--help",

58 action=HelpAction,

59 dest="help",

60 help="Show help message and configuration info",

61 )

62 group._addoption(

63 "-p",

64 action="append",

65 dest="plugins",

66 default=[],

67 metavar="name",

68 help="Early-load given plugin module name or entry point (multi-allowed). "

69 "To avoid loading of plugins, use the `no:` prefix, e.g. "

70 "`no:doctest`.",

71 )

72 group.addoption(

73 "--traceconfig",

74 "--trace-config",

75 action="store_true",

76 default=False,

77 help="Trace considerations of conftest.py files",

78 )

79 group.addoption(

80 "--debug",

81 action="store",

82 nargs="?",

83 const="pytestdebug.log",

84 dest="debug",

85 metavar="DEBUG_FILE_NAME",

86 help="Store internal tracing debug information in this log file. "

87 "This file is opened with 'w' and truncated as a result, care advised. "

88 "Default: pytestdebug.log.",

89 )

90 group._addoption(

91 "-o",

92 "--override-ini",

93 dest="override_ini",

94 action="append",

95 help='Override ini option with "option=value" style, '

96 "e.g. `-o xfail_strict=True -o cache_dir=cache`.",

97 )

98

99

100@pytest.hookimpl(hookwrapper=True)

101 def pytest_cmdline_parse():

102 outcome = yield

103 config: Config = outcome.get_result()

104

105 if config.option.debug:

106 # --debug | --debug <file.log> was provided.

107 path = config.option.debug

108 debugfile = open(path, "w")

109 debugfile.write(

110 "versions pytest-%s, "

111 "python-%s\ncwd=%s\nargs=%s\n\n"

112 % (

113 pytest.__version__,

114 ".".join(map(str, sys.version_info)),

115 os.getcwd(),

116 config.invocation_params.args,

117 )

118 )

119 config.trace.root.setwriter(debugfile.write)

120 undo_tracing = config.pluginmanager.enable_tracing()

121 sys.stderr.write("writing pytest debug information to %s\n" % path)

122

123 def unset_tracing() -> None:

124 debugfile.close()

125 sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)

126 config.trace.root.setwriter(None)

127 undo_tracing()

128

129 config.add_cleanup(unset_tracing)

130

131

132 def showversion(config: Config) -> None:

133 if config.option.version > 1:

134 sys.stdout.write(

135 "This is pytest version {}, imported from {}\n".format(

136 pytest.__version__, pytest.__file__

137 )

138 )

139 plugininfo = getpluginversioninfo(config)

140 if plugininfo:

141 for line in plugininfo:

142 sys.stdout.write(line + "\n")

143 else:

144 sys.stdout.write(f"pytest {pytest.__version__}\n")

145

146

147 def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:

148 if config.option.version > 0:

149 showversion(config)

150 return 0

151 elif config.option.help:

152 config._do_configure()

153 showhelp(config)

154 config._ensure_unconfigure()

155 return 0

156 return None

157

158

159 def showhelp(config: Config) -> None:

160 import textwrap

161

162 reporter = config.pluginmanager.get_plugin("terminalreporter")

163 tw = reporter._tw

164 tw.write(config._parser.optparser.format_help())

165 tw.line()

166 tw.line(

167 "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"

168 )

169 tw.line()

170

171 columns = tw.fullwidth # costly call

172 indent_len = 24 # based on argparse's max_help_position=24

173 indent = " " * indent_len

174 for name in config._parser._ininames:

175 help, type, default = config._parser._inidict[name]

176 if type is None:

177 type = "string"

178 if help is None:

179 raise TypeError(f"help argument cannot be None for {name}")

180 spec = f"{name} ({type}):"

181 tw.write(" %s" % spec)

182 spec_len = len(spec)

183 if spec_len > (indent_len - 3):

184 # Display help starting at a new line.

185 tw.line()

186 helplines = textwrap.wrap(

187 help,

188 columns,

189 initial_indent=indent,

190 subsequent_indent=indent,

191 break_on_hyphens=False,

192 )

193

194 for line in helplines:

195 tw.line(line)

196 else:

197 # Display help starting after the spec, following lines indented.

198 tw.write(" " * (indent_len - spec_len - 2))

199 wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)

200

201 if wrapped:

202 tw.line(wrapped[0])

203 for line in wrapped[1:]:

204 tw.line(indent + line)

205

206 tw.line()

207 tw.line("Environment variables:")

208 vars = [

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 ]

214 for name, help in vars:

215 tw.line(f" {name:<24} {help}")

216 tw.line()

217 tw.line()

218

219 tw.line("to see available markers type: pytest --markers")

220 tw.line("to see available fixtures type: pytest --fixtures")

221 tw.line(

222 "(shown according to specified file_or_dir or current dir "

223 "if not specified; fixtures with leading '_' are only shown "

224 "with the '-v' option"

225 )

226

227 for warningreport in reporter.stats.get("warnings", []):

228 tw.line("warning : " + warningreport.message, red=True)

229 return

230

231

232 conftest_options = [("pytest_plugins", "list of plugin names to load")]

233

234

235 def getpluginversioninfo(config: Config) -> List[str]:

236 lines = []

237 plugininfo = config.pluginmanager.list_plugin_distinfo()

238 if plugininfo:

239 lines.append("setuptools registered plugins:")

240 for plugin, dist in plugininfo:

241 loc = getattr(plugin, "__file__", repr(plugin))

242 content = f"{dist.project_name}-{dist.version} at {loc}"

243 lines.append(" " + content)

244 return lines

245

246

247 def pytest_report_header(config: Config) -> List[str]:

248 lines = []

249 if config.option.debug or config.option.traceconfig:

250 lines.append(f"using: pytest-{pytest.__version__}")

251

252 verinfo = getpluginversioninfo(config)

253 if verinfo:

254 lines.extend(verinfo)

255

256 if config.option.traceconfig:

257 lines.append("active plugins:")

258 items = config.pluginmanager.list_name_plugin()

259 for name, plugin in items:

260 if hasattr(plugin, "__file__"):

261 r = plugin.__file__

262 else:

263 r = repr(plugin)

264 lines.append(f" {name:<20}: {r}")

265 return lines