Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import argparse 

2import sys 

3import warnings 

4from gettext import gettext 

5from typing import Any 

6from typing import Callable 

7from typing import cast 

8from typing import Dict 

9from typing import List 

10from typing import Mapping 

11from typing import Optional 

12from typing import Sequence 

13from typing import Tuple 

14from typing import Union 

15 

16import py 

17 

18import _pytest._io 

19from _pytest.compat import TYPE_CHECKING 

20from _pytest.config.exceptions import UsageError 

21 

22if TYPE_CHECKING: 

23 from typing import NoReturn 

24 from typing_extensions import Literal 

25 

26FILE_OR_DIR = "file_or_dir" 

27 

28 

29class Parser: 

30 """ Parser for command line arguments and ini-file values. 

31 

32 :ivar extra_info: dict of generic param -> value to display in case 

33 there's an error processing the command line arguments. 

34 """ 

35 

36 prog = None # type: Optional[str] 

37 

38 def __init__( 

39 self, 

40 usage: Optional[str] = None, 

41 processopt: Optional[Callable[["Argument"], None]] = None, 

42 ) -> None: 

43 self._anonymous = OptionGroup("custom options", parser=self) 

44 self._groups = [] # type: List[OptionGroup] 

45 self._processopt = processopt 

46 self._usage = usage 

47 self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]] 

48 self._ininames = [] # type: List[str] 

49 self.extra_info = {} # type: Dict[str, Any] 

50 

51 def processoption(self, option: "Argument") -> None: 

52 if self._processopt: 

53 if option.dest: 

54 self._processopt(option) 

55 

56 def getgroup( 

57 self, name: str, description: str = "", after: Optional[str] = None 

58 ) -> "OptionGroup": 

59 """ get (or create) a named option Group. 

60 

61 :name: name of the option group. 

62 :description: long description for --help output. 

63 :after: name of other group, used for ordering --help output. 

64 

65 The returned group object has an ``addoption`` method with the same 

66 signature as :py:func:`parser.addoption 

67 <_pytest.config.argparsing.Parser.addoption>` but will be shown in the 

68 respective group in the output of ``pytest. --help``. 

69 """ 

70 for group in self._groups: 

71 if group.name == name: 

72 return group 

73 group = OptionGroup(name, description, parser=self) 

74 i = 0 

75 for i, grp in enumerate(self._groups): 

76 if grp.name == after: 

77 break 

78 self._groups.insert(i + 1, group) 

79 return group 

80 

81 def addoption(self, *opts: str, **attrs: Any) -> None: 

82 """ register a command line option. 

83 

84 :opts: option names, can be short or long options. 

85 :attrs: same attributes which the ``add_argument()`` function of the 

86 `argparse library 

87 <https://docs.python.org/library/argparse.html>`_ 

88 accepts. 

89 

90 After command line parsing options are available on the pytest config 

91 object via ``config.option.NAME`` where ``NAME`` is usually set 

92 by passing a ``dest`` attribute, for example 

93 ``addoption("--long", dest="NAME", ...)``. 

94 """ 

95 self._anonymous.addoption(*opts, **attrs) 

96 

97 def parse( 

98 self, 

99 args: Sequence[Union[str, py.path.local]], 

100 namespace: Optional[argparse.Namespace] = None, 

101 ) -> argparse.Namespace: 

102 from _pytest._argcomplete import try_argcomplete 

103 

104 self.optparser = self._getparser() 

105 try_argcomplete(self.optparser) 

106 strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] 

107 return self.optparser.parse_args(strargs, namespace=namespace) 

108 

109 def _getparser(self) -> "MyOptionParser": 

110 from _pytest._argcomplete import filescompleter 

111 

112 optparser = MyOptionParser(self, self.extra_info, prog=self.prog) 

113 groups = self._groups + [self._anonymous] 

114 for group in groups: 

115 if group.options: 

116 desc = group.description or group.name 

117 arggroup = optparser.add_argument_group(desc) 

118 for option in group.options: 

119 n = option.names() 

120 a = option.attrs() 

121 arggroup.add_argument(*n, **a) 

122 file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") 

123 # bash like autocompletion for dirs (appending '/') 

124 # Type ignored because typeshed doesn't know about argcomplete. 

125 file_or_dir_arg.completer = filescompleter # type: ignore 

126 return optparser 

127 

128 def parse_setoption( 

129 self, 

130 args: Sequence[Union[str, py.path.local]], 

131 option: argparse.Namespace, 

132 namespace: Optional[argparse.Namespace] = None, 

133 ) -> List[str]: 

134 parsedoption = self.parse(args, namespace=namespace) 

135 for name, value in parsedoption.__dict__.items(): 

136 setattr(option, name, value) 

137 return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) 

138 

139 def parse_known_args( 

140 self, 

141 args: Sequence[Union[str, py.path.local]], 

142 namespace: Optional[argparse.Namespace] = None, 

143 ) -> argparse.Namespace: 

144 """parses and returns a namespace object with known arguments at this 

145 point. 

146 """ 

147 return self.parse_known_and_unknown_args(args, namespace=namespace)[0] 

148 

149 def parse_known_and_unknown_args( 

150 self, 

151 args: Sequence[Union[str, py.path.local]], 

152 namespace: Optional[argparse.Namespace] = None, 

153 ) -> Tuple[argparse.Namespace, List[str]]: 

154 """parses and returns a namespace object with known arguments, and 

155 the remaining arguments unknown at this point. 

156 """ 

157 optparser = self._getparser() 

158 strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] 

159 return optparser.parse_known_args(strargs, namespace=namespace) 

160 

161 def addini( 

162 self, 

163 name: str, 

164 help: str, 

165 type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None, 

166 default=None, 

167 ) -> None: 

168 """ register an ini-file option. 

169 

170 :name: name of the ini-variable 

171 :type: type of the variable, can be ``pathlist``, ``args``, ``linelist`` 

172 or ``bool``. 

173 :default: default value if no ini-file option exists but is queried. 

174 

175 The value of ini-variables can be retrieved via a call to 

176 :py:func:`config.getini(name) <_pytest.config.Config.getini>`. 

177 """ 

178 assert type in (None, "pathlist", "args", "linelist", "bool") 

179 self._inidict[name] = (help, type, default) 

180 self._ininames.append(name) 

181 

182 

183class ArgumentError(Exception): 

184 """ 

185 Raised if an Argument instance is created with invalid or 

186 inconsistent arguments. 

187 """ 

188 

189 def __init__(self, msg: str, option: Union["Argument", str]) -> None: 

190 self.msg = msg 

191 self.option_id = str(option) 

192 

193 def __str__(self) -> str: 

194 if self.option_id: 

195 return "option {}: {}".format(self.option_id, self.msg) 

196 else: 

197 return self.msg 

198 

199 

200class Argument: 

201 """class that mimics the necessary behaviour of optparse.Option 

202 

203 it's currently a least effort implementation 

204 and ignoring choices and integer prefixes 

205 https://docs.python.org/3/library/optparse.html#optparse-standard-option-types 

206 """ 

207 

208 _typ_map = {"int": int, "string": str, "float": float, "complex": complex} 

209 

210 def __init__(self, *names: str, **attrs: Any) -> None: 

211 """store parms in private vars for use in add_argument""" 

212 self._attrs = attrs 

213 self._short_opts = [] # type: List[str] 

214 self._long_opts = [] # type: List[str] 

215 if "%default" in (attrs.get("help") or ""): 

216 warnings.warn( 

217 'pytest now uses argparse. "%default" should be' 

218 ' changed to "%(default)s" ', 

219 DeprecationWarning, 

220 stacklevel=3, 

221 ) 

222 try: 

223 typ = attrs["type"] 

224 except KeyError: 

225 pass 

226 else: 

227 # this might raise a keyerror as well, don't want to catch that 

228 if isinstance(typ, str): 

229 if typ == "choice": 

230 warnings.warn( 

231 "`type` argument to addoption() is the string %r." 

232 " For choices this is optional and can be omitted, " 

233 " but when supplied should be a type (for example `str` or `int`)." 

234 " (options: %s)" % (typ, names), 

235 DeprecationWarning, 

236 stacklevel=4, 

237 ) 

238 # argparse expects a type here take it from 

239 # the type of the first element 

240 attrs["type"] = type(attrs["choices"][0]) 

241 else: 

242 warnings.warn( 

243 "`type` argument to addoption() is the string %r, " 

244 " but when supplied should be a type (for example `str` or `int`)." 

245 " (options: %s)" % (typ, names), 

246 DeprecationWarning, 

247 stacklevel=4, 

248 ) 

249 attrs["type"] = Argument._typ_map[typ] 

250 # used in test_parseopt -> test_parse_defaultgetter 

251 self.type = attrs["type"] 

252 else: 

253 self.type = typ 

254 try: 

255 # attribute existence is tested in Config._processopt 

256 self.default = attrs["default"] 

257 except KeyError: 

258 pass 

259 self._set_opt_strings(names) 

260 dest = attrs.get("dest") # type: Optional[str] 

261 if dest: 

262 self.dest = dest 

263 elif self._long_opts: 

264 self.dest = self._long_opts[0][2:].replace("-", "_") 

265 else: 

266 try: 

267 self.dest = self._short_opts[0][1:] 

268 except IndexError as e: 

269 self.dest = "???" # Needed for the error repr. 

270 raise ArgumentError("need a long or short option", self) from e 

271 

272 def names(self) -> List[str]: 

273 return self._short_opts + self._long_opts 

274 

275 def attrs(self) -> Mapping[str, Any]: 

276 # update any attributes set by processopt 

277 attrs = "default dest help".split() 

278 attrs.append(self.dest) 

279 for attr in attrs: 

280 try: 

281 self._attrs[attr] = getattr(self, attr) 

282 except AttributeError: 

283 pass 

284 if self._attrs.get("help"): 

285 a = self._attrs["help"] 

286 a = a.replace("%default", "%(default)s") 

287 # a = a.replace('%prog', '%(prog)s') 

288 self._attrs["help"] = a 

289 return self._attrs 

290 

291 def _set_opt_strings(self, opts: Sequence[str]) -> None: 

292 """directly from optparse 

293 

294 might not be necessary as this is passed to argparse later on""" 

295 for opt in opts: 

296 if len(opt) < 2: 

297 raise ArgumentError( 

298 "invalid option string %r: " 

299 "must be at least two characters long" % opt, 

300 self, 

301 ) 

302 elif len(opt) == 2: 

303 if not (opt[0] == "-" and opt[1] != "-"): 

304 raise ArgumentError( 

305 "invalid short option string %r: " 

306 "must be of the form -x, (x any non-dash char)" % opt, 

307 self, 

308 ) 

309 self._short_opts.append(opt) 

310 else: 

311 if not (opt[0:2] == "--" and opt[2] != "-"): 

312 raise ArgumentError( 

313 "invalid long option string %r: " 

314 "must start with --, followed by non-dash" % opt, 

315 self, 

316 ) 

317 self._long_opts.append(opt) 

318 

319 def __repr__(self) -> str: 

320 args = [] # type: List[str] 

321 if self._short_opts: 

322 args += ["_short_opts: " + repr(self._short_opts)] 

323 if self._long_opts: 

324 args += ["_long_opts: " + repr(self._long_opts)] 

325 args += ["dest: " + repr(self.dest)] 

326 if hasattr(self, "type"): 

327 args += ["type: " + repr(self.type)] 

328 if hasattr(self, "default"): 

329 args += ["default: " + repr(self.default)] 

330 return "Argument({})".format(", ".join(args)) 

331 

332 

333class OptionGroup: 

334 def __init__( 

335 self, name: str, description: str = "", parser: Optional[Parser] = None 

336 ) -> None: 

337 self.name = name 

338 self.description = description 

339 self.options = [] # type: List[Argument] 

340 self.parser = parser 

341 

342 def addoption(self, *optnames: str, **attrs: Any) -> None: 

343 """ add an option to this group. 

344 

345 if a shortened version of a long option is specified it will 

346 be suppressed in the help. addoption('--twowords', '--two-words') 

347 results in help showing '--two-words' only, but --twowords gets 

348 accepted **and** the automatic destination is in args.twowords 

349 """ 

350 conflict = set(optnames).intersection( 

351 name for opt in self.options for name in opt.names() 

352 ) 

353 if conflict: 

354 raise ValueError("option names %s already added" % conflict) 

355 option = Argument(*optnames, **attrs) 

356 self._addoption_instance(option, shortupper=False) 

357 

358 def _addoption(self, *optnames: str, **attrs: Any) -> None: 

359 option = Argument(*optnames, **attrs) 

360 self._addoption_instance(option, shortupper=True) 

361 

362 def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: 

363 if not shortupper: 

364 for opt in option._short_opts: 

365 if opt[0] == "-" and opt[1].islower(): 

366 raise ValueError("lowercase shortoptions reserved") 

367 if self.parser: 

368 self.parser.processoption(option) 

369 self.options.append(option) 

370 

371 

372class MyOptionParser(argparse.ArgumentParser): 

373 def __init__( 

374 self, 

375 parser: Parser, 

376 extra_info: Optional[Dict[str, Any]] = None, 

377 prog: Optional[str] = None, 

378 ) -> None: 

379 self._parser = parser 

380 argparse.ArgumentParser.__init__( 

381 self, 

382 prog=prog, 

383 usage=parser._usage, 

384 add_help=False, 

385 formatter_class=DropShorterLongHelpFormatter, 

386 allow_abbrev=False, 

387 ) 

388 # extra_info is a dict of (param -> value) to display if there's 

389 # an usage error to provide more contextual information to the user 

390 self.extra_info = extra_info if extra_info else {} 

391 

392 def error(self, message: str) -> "NoReturn": 

393 """Transform argparse error message into UsageError.""" 

394 msg = "{}: error: {}".format(self.prog, message) 

395 

396 if hasattr(self._parser, "_config_source_hint"): 

397 # Type ignored because the attribute is set dynamically. 

398 msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore 

399 

400 raise UsageError(self.format_usage() + msg) 

401 

402 # Type ignored because typeshed has a very complex type in the superclass. 

403 def parse_args( # type: ignore 

404 self, 

405 args: Optional[Sequence[str]] = None, 

406 namespace: Optional[argparse.Namespace] = None, 

407 ) -> argparse.Namespace: 

408 """allow splitting of positional arguments""" 

409 parsed, unrecognized = self.parse_known_args(args, namespace) 

410 if unrecognized: 

411 for arg in unrecognized: 

412 if arg and arg[0] == "-": 

413 lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] 

414 for k, v in sorted(self.extra_info.items()): 

415 lines.append(" {}: {}".format(k, v)) 

416 self.error("\n".join(lines)) 

417 getattr(parsed, FILE_OR_DIR).extend(unrecognized) 

418 return parsed 

419 

420 if sys.version_info[:2] < (3, 9): # pragma: no cover 

421 # Backport of https://github.com/python/cpython/pull/14316 so we can 

422 # disable long --argument abbreviations without breaking short flags. 

423 def _parse_optional( 

424 self, arg_string: str 

425 ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: 

426 if not arg_string: 

427 return None 

428 if not arg_string[0] in self.prefix_chars: 

429 return None 

430 if arg_string in self._option_string_actions: 

431 action = self._option_string_actions[arg_string] 

432 return action, arg_string, None 

433 if len(arg_string) == 1: 

434 return None 

435 if "=" in arg_string: 

436 option_string, explicit_arg = arg_string.split("=", 1) 

437 if option_string in self._option_string_actions: 

438 action = self._option_string_actions[option_string] 

439 return action, option_string, explicit_arg 

440 if self.allow_abbrev or not arg_string.startswith("--"): 

441 option_tuples = self._get_option_tuples(arg_string) 

442 if len(option_tuples) > 1: 

443 msg = gettext( 

444 "ambiguous option: %(option)s could match %(matches)s" 

445 ) 

446 options = ", ".join(option for _, option, _ in option_tuples) 

447 self.error(msg % {"option": arg_string, "matches": options}) 

448 elif len(option_tuples) == 1: 

449 (option_tuple,) = option_tuples 

450 return option_tuple 

451 if self._negative_number_matcher.match(arg_string): 

452 if not self._has_negative_number_optionals: 

453 return None 

454 if " " in arg_string: 

455 return None 

456 return None, arg_string, None 

457 

458 

459class DropShorterLongHelpFormatter(argparse.HelpFormatter): 

460 """shorten help for long options that differ only in extra hyphens 

461 

462 - collapse **long** options that are the same except for extra hyphens 

463 - shortcut if there are only two options and one of them is a short one 

464 - cache result on action object as this is called at least 2 times 

465 """ 

466 

467 def __init__(self, *args: Any, **kwargs: Any) -> None: 

468 """Use more accurate terminal width via pylib.""" 

469 if "width" not in kwargs: 

470 kwargs["width"] = _pytest._io.get_terminal_width() 

471 super().__init__(*args, **kwargs) 

472 

473 def _format_action_invocation(self, action: argparse.Action) -> str: 

474 orgstr = argparse.HelpFormatter._format_action_invocation(self, action) 

475 if orgstr and orgstr[0] != "-": # only optional arguments 

476 return orgstr 

477 res = getattr( 

478 action, "_formatted_action_invocation", None 

479 ) # type: Optional[str] 

480 if res: 

481 return res 

482 options = orgstr.split(", ") 

483 if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): 

484 # a shortcut for '-h, --help' or '--abc', '-a' 

485 action._formatted_action_invocation = orgstr # type: ignore 

486 return orgstr 

487 return_list = [] 

488 short_long = {} # type: Dict[str, str] 

489 for option in options: 

490 if len(option) == 2 or option[2] == " ": 

491 continue 

492 if not option.startswith("--"): 

493 raise ArgumentError( 

494 'long optional argument without "--": [%s]' % (option), option 

495 ) 

496 xxoption = option[2:] 

497 shortened = xxoption.replace("-", "") 

498 if shortened not in short_long or len(short_long[shortened]) < len( 

499 xxoption 

500 ): 

501 short_long[shortened] = xxoption 

502 # now short_long has been filled out to the longest with dashes 

503 # **and** we keep the right option ordering from add_argument 

504 for option in options: 

505 if len(option) == 2 or option[2] == " ": 

506 return_list.append(option) 

507 if option[2:] == short_long.get(option.replace("-", "")): 

508 return_list.append(option.replace(" ", "=", 1)) 

509 formatted_action_invocation = ", ".join(return_list) 

510 action._formatted_action_invocation = formatted_action_invocation # type: ignore 

511 return formatted_action_invocation 

512 

513 def _split_lines(self, text, width): 

514 """Wrap lines after splitting on original newlines. 

515 

516 This allows to have explicit line breaks in the help text. 

517 """ 

518 import textwrap 

519 

520 lines = [] 

521 for line in text.splitlines(): 

522 lines.extend(textwrap.wrap(line.strip(), width)) 

523 return lines