Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/config/argparsing.py: 69%

259 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1import argparse 

2import os 

3import sys 

4import warnings 

5from gettext import gettext 

6from typing import Any 

7from typing import Callable 

8from typing import cast 

9from typing import Dict 

10from typing import List 

11from typing import Mapping 

12from typing import NoReturn 

13from typing import Optional 

14from typing import Sequence 

15from typing import Tuple 

16from typing import TYPE_CHECKING 

17from typing import Union 

18 

19import _pytest._io 

20from _pytest.compat import final 

21from _pytest.config.exceptions import UsageError 

22from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT 

23from _pytest.deprecated import ARGUMENT_TYPE_STR 

24from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE 

25from _pytest.deprecated import check_ispytest 

26 

27if TYPE_CHECKING: 

28 from typing_extensions import Literal 

29 

30FILE_OR_DIR = "file_or_dir" 

31 

32 

33@final 

34class Parser: 

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

36 

37 :ivar extra_info: Dict of generic param -> value to display in case 

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

39 """ 

40 

41 prog: Optional[str] = None 

42 

43 def __init__( 

44 self, 

45 usage: Optional[str] = None, 

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

47 *, 

48 _ispytest: bool = False, 

49 ) -> None: 

50 check_ispytest(_ispytest) 

51 self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True) 

52 self._groups: List[OptionGroup] = [] 

53 self._processopt = processopt 

54 self._usage = usage 

55 self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} 

56 self._ininames: List[str] = [] 

57 self.extra_info: Dict[str, Any] = {} 

58 

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

60 if self._processopt: 

61 if option.dest: 

62 self._processopt(option) 

63 

64 def getgroup( 

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

66 ) -> "OptionGroup": 

67 """Get (or create) a named option Group. 

68 

69 :param name: Name of the option group. 

70 :param description: Long description for --help output. 

71 :param after: Name of another group, used for ordering --help output. 

72 :returns: The option group. 

73 

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

75 signature as :func:`parser.addoption <pytest.Parser.addoption>` but 

76 will be shown in the respective group in the output of 

77 ``pytest --help``. 

78 """ 

79 for group in self._groups: 

80 if group.name == name: 

81 return group 

82 group = OptionGroup(name, description, parser=self, _ispytest=True) 

83 i = 0 

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

85 if grp.name == after: 

86 break 

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

88 return group 

89 

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

91 """Register a command line option. 

92 

93 :param opts: 

94 Option names, can be short or long options. 

95 :param attrs: 

96 Same attributes as the argparse library's :py:func:`add_argument() 

97 <argparse.ArgumentParser.add_argument>` function accepts. 

98 

99 After command line parsing, options are available on the pytest config 

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

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

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

103 """ 

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

105 

106 def parse( 

107 self, 

108 args: Sequence[Union[str, "os.PathLike[str]"]], 

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

110 ) -> argparse.Namespace: 

111 from _pytest._argcomplete import try_argcomplete 

112 

113 self.optparser = self._getparser() 

114 try_argcomplete(self.optparser) 

115 strargs = [os.fspath(x) for x in args] 

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

117 

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

119 from _pytest._argcomplete import filescompleter 

120 

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

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

123 for group in groups: 

124 if group.options: 

125 desc = group.description or group.name 

126 arggroup = optparser.add_argument_group(desc) 

127 for option in group.options: 

128 n = option.names() 

129 a = option.attrs() 

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

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

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

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

134 file_or_dir_arg.completer = filescompleter # type: ignore 

135 return optparser 

136 

137 def parse_setoption( 

138 self, 

139 args: Sequence[Union[str, "os.PathLike[str]"]], 

140 option: argparse.Namespace, 

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

142 ) -> List[str]: 

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

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

145 setattr(option, name, value) 

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

147 

148 def parse_known_args( 

149 self, 

150 args: Sequence[Union[str, "os.PathLike[str]"]], 

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

152 ) -> argparse.Namespace: 

153 """Parse the known arguments at this point. 

154 

155 :returns: An argparse namespace object. 

156 """ 

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

158 

159 def parse_known_and_unknown_args( 

160 self, 

161 args: Sequence[Union[str, "os.PathLike[str]"]], 

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

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

164 """Parse the known arguments at this point, and also return the 

165 remaining unknown arguments. 

166 

167 :returns: 

168 A tuple containing an argparse namespace object for the known 

169 arguments, and a list of the unknown arguments. 

170 """ 

171 optparser = self._getparser() 

172 strargs = [os.fspath(x) for x in args] 

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

174 

175 def addini( 

176 self, 

177 name: str, 

178 help: str, 

179 type: Optional[ 

180 "Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']" 

181 ] = None, 

182 default: Any = None, 

183 ) -> None: 

184 """Register an ini-file option. 

185 

186 :param name: 

187 Name of the ini-variable. 

188 :param type: 

189 Type of the variable. Can be: 

190 

191 * ``string``: a string 

192 * ``bool``: a boolean 

193 * ``args``: a list of strings, separated as in a shell 

194 * ``linelist``: a list of strings, separated by line breaks 

195 * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell 

196 * ``pathlist``: a list of ``py.path``, separated as in a shell 

197 

198 .. versionadded:: 7.0 

199 The ``paths`` variable type. 

200 

201 Defaults to ``string`` if ``None`` or not passed. 

202 :param default: 

203 Default value if no ini-file option exists but is queried. 

204 

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

206 :py:func:`config.getini(name) <pytest.Config.getini>`. 

207 """ 

208 assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool") 

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

210 self._ininames.append(name) 

211 

212 

213class ArgumentError(Exception): 

214 """Raised if an Argument instance is created with invalid or 

215 inconsistent arguments.""" 

216 

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

218 self.msg = msg 

219 self.option_id = str(option) 

220 

221 def __str__(self) -> str: 

222 if self.option_id: 

223 return f"option {self.option_id}: {self.msg}" 

224 else: 

225 return self.msg 

226 

227 

228class Argument: 

229 """Class that mimics the necessary behaviour of optparse.Option. 

230 

231 It's currently a least effort implementation and ignoring choices 

232 and integer prefixes. 

233 

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

235 """ 

236 

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

238 

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

240 """Store params in private vars for use in add_argument.""" 

241 self._attrs = attrs 

242 self._short_opts: List[str] = [] 

243 self._long_opts: List[str] = [] 

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

245 warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3) 

246 try: 

247 typ = attrs["type"] 

248 except KeyError: 

249 pass 

250 else: 

251 # This might raise a keyerror as well, don't want to catch that. 

252 if isinstance(typ, str): 

253 if typ == "choice": 

254 warnings.warn( 

255 ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names), 

256 stacklevel=4, 

257 ) 

258 # argparse expects a type here take it from 

259 # the type of the first element 

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

261 else: 

262 warnings.warn( 

263 ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4 

264 ) 

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

266 # Used in test_parseopt -> test_parse_defaultgetter. 

267 self.type = attrs["type"] 

268 else: 

269 self.type = typ 

270 try: 

271 # Attribute existence is tested in Config._processopt. 

272 self.default = attrs["default"] 

273 except KeyError: 

274 pass 

275 self._set_opt_strings(names) 

276 dest: Optional[str] = attrs.get("dest") 

277 if dest: 

278 self.dest = dest 

279 elif self._long_opts: 

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

281 else: 

282 try: 

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

284 except IndexError as e: 

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

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

287 

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

289 return self._short_opts + self._long_opts 

290 

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

292 # Update any attributes set by processopt. 

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

294 attrs.append(self.dest) 

295 for attr in attrs: 

296 try: 

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

298 except AttributeError: 

299 pass 

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

301 a = self._attrs["help"] 

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

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

304 self._attrs["help"] = a 

305 return self._attrs 

306 

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

308 """Directly from optparse. 

309 

310 Might not be necessary as this is passed to argparse later on. 

311 """ 

312 for opt in opts: 

313 if len(opt) < 2: 

314 raise ArgumentError( 

315 "invalid option string %r: " 

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

317 self, 

318 ) 

319 elif len(opt) == 2: 

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

321 raise ArgumentError( 

322 "invalid short option string %r: " 

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

324 self, 

325 ) 

326 self._short_opts.append(opt) 

327 else: 

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

329 raise ArgumentError( 

330 "invalid long option string %r: " 

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

332 self, 

333 ) 

334 self._long_opts.append(opt) 

335 

336 def __repr__(self) -> str: 

337 args: List[str] = [] 

338 if self._short_opts: 

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

340 if self._long_opts: 

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

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

343 if hasattr(self, "type"): 

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

345 if hasattr(self, "default"): 

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

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

348 

349 

350class OptionGroup: 

351 """A group of options shown in its own section.""" 

352 

353 def __init__( 

354 self, 

355 name: str, 

356 description: str = "", 

357 parser: Optional[Parser] = None, 

358 *, 

359 _ispytest: bool = False, 

360 ) -> None: 

361 check_ispytest(_ispytest) 

362 self.name = name 

363 self.description = description 

364 self.options: List[Argument] = [] 

365 self.parser = parser 

366 

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

368 """Add an option to this group. 

369 

370 If a shortened version of a long option is specified, it will 

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

372 results in help showing ``--two-words`` only, but ``--twowords`` gets 

373 accepted **and** the automatic destination is in ``args.twowords``. 

374 

375 :param opts: 

376 Option names, can be short or long options. 

377 :param attrs: 

378 Same attributes as the argparse library's :py:func:`add_argument() 

379 <argparse.ArgumentParser.add_argument>` function accepts. 

380 """ 

381 conflict = set(opts).intersection( 

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

383 ) 

384 if conflict: 

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

386 option = Argument(*opts, **attrs) 

387 self._addoption_instance(option, shortupper=False) 

388 

389 def _addoption(self, *opts: str, **attrs: Any) -> None: 

390 option = Argument(*opts, **attrs) 

391 self._addoption_instance(option, shortupper=True) 

392 

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

394 if not shortupper: 

395 for opt in option._short_opts: 

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

397 raise ValueError("lowercase shortoptions reserved") 

398 if self.parser: 

399 self.parser.processoption(option) 

400 self.options.append(option) 

401 

402 

403class MyOptionParser(argparse.ArgumentParser): 

404 def __init__( 

405 self, 

406 parser: Parser, 

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

408 prog: Optional[str] = None, 

409 ) -> None: 

410 self._parser = parser 

411 super().__init__( 

412 prog=prog, 

413 usage=parser._usage, 

414 add_help=False, 

415 formatter_class=DropShorterLongHelpFormatter, 

416 allow_abbrev=False, 

417 ) 

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

419 # an usage error to provide more contextual information to the user. 

420 self.extra_info = extra_info if extra_info else {} 

421 

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

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

424 msg = f"{self.prog}: error: {message}" 

425 

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

427 # Type ignored because the attribute is set dynamically. 

428 msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore 

429 

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

431 

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

433 def parse_args( # type: ignore 

434 self, 

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

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

437 ) -> argparse.Namespace: 

438 """Allow splitting of positional arguments.""" 

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

440 if unrecognized: 

441 for arg in unrecognized: 

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

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

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

445 lines.append(f" {k}: {v}") 

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

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

448 return parsed 

449 

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

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

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

453 def _parse_optional( 

454 self, arg_string: str 

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

456 if not arg_string: 

457 return None 

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

459 return None 

460 if arg_string in self._option_string_actions: 

461 action = self._option_string_actions[arg_string] 

462 return action, arg_string, None 

463 if len(arg_string) == 1: 

464 return None 

465 if "=" in arg_string: 

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

467 if option_string in self._option_string_actions: 

468 action = self._option_string_actions[option_string] 

469 return action, option_string, explicit_arg 

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

471 option_tuples = self._get_option_tuples(arg_string) 

472 if len(option_tuples) > 1: 

473 msg = gettext( 

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

475 ) 

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

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

478 elif len(option_tuples) == 1: 

479 (option_tuple,) = option_tuples 

480 return option_tuple 

481 if self._negative_number_matcher.match(arg_string): 

482 if not self._has_negative_number_optionals: 

483 return None 

484 if " " in arg_string: 

485 return None 

486 return None, arg_string, None 

487 

488 

489class DropShorterLongHelpFormatter(argparse.HelpFormatter): 

490 """Shorten help for long options that differ only in extra hyphens. 

491 

492 - Collapse **long** options that are the same except for extra hyphens. 

493 - Shortcut if there are only two options and one of them is a short one. 

494 - Cache result on the action object as this is called at least 2 times. 

495 """ 

496 

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

498 # Use more accurate terminal width. 

499 if "width" not in kwargs: 

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

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

502 

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

504 orgstr = super()._format_action_invocation(action) 

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

506 return orgstr 

507 res: Optional[str] = getattr(action, "_formatted_action_invocation", None) 

508 if res: 

509 return res 

510 options = orgstr.split(", ") 

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

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

513 action._formatted_action_invocation = orgstr # type: ignore 

514 return orgstr 

515 return_list = [] 

516 short_long: Dict[str, str] = {} 

517 for option in options: 

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

519 continue 

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

521 raise ArgumentError( 

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

523 ) 

524 xxoption = option[2:] 

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

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

527 xxoption 

528 ): 

529 short_long[shortened] = xxoption 

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

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

532 for option in options: 

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

534 return_list.append(option) 

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

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

537 formatted_action_invocation = ", ".join(return_list) 

538 action._formatted_action_invocation = formatted_action_invocation # type: ignore 

539 return formatted_action_invocation 

540 

541 def _split_lines(self, text, width): 

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

543 

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

545 """ 

546 import textwrap 

547 

548 lines = [] 

549 for line in text.splitlines(): 

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

551 return lines