Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/parser.py: 58%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

496 statements  

1import sys 

2import shlex 

3import os.path 

4from collections import OrderedDict 

5from typing import Optional 

6 

7from boltons.iterutils import split, unique 

8from boltons.dictutils import OrderedMultiDict as OMD 

9from boltons.funcutils import format_exp_repr, format_nonexp_repr 

10 

11from face.utils import (ERROR, 

12 get_type_desc, 

13 flag_to_identifier, 

14 normalize_flag_name, 

15 process_command_name, 

16 get_minimal_executable) 

17from face.errors import (FaceException, 

18 ArgumentParseError, 

19 ArgumentArityError, 

20 InvalidSubcommand, 

21 UnknownFlag, 

22 DuplicateFlag, 

23 InvalidFlagArgument, 

24 InvalidPositionalArgument, 

25 MissingRequiredFlags) 

26 

27 

28def _arg_to_subcmd(arg): 

29 return arg.lower().replace('-', '_') 

30 

31 

32def _multi_error(flag, arg_val_list): 

33 "Raise a DuplicateFlag if more than one value is specified for an argument" 

34 if len(arg_val_list) > 1: 

35 raise DuplicateFlag.from_parse(flag, arg_val_list) 

36 return arg_val_list[0] 

37 

38 

39def _multi_extend(flag, arg_val_list): 

40 "Return a list of all arguments specified for a flag" 

41 ret = [v for v in arg_val_list if v is not flag.missing] 

42 return ret 

43 

44 

45def _multi_override(flag, arg_val_list): 

46 "Return only the last argument specified for a flag" 

47 return arg_val_list[-1] 

48 

49# TODO: _multi_ignore? 

50 

51_MULTI_SHORTCUTS = {'error': _multi_error, 

52 False: _multi_error, 

53 'extend': _multi_extend, 

54 True: _multi_extend, 

55 'override': _multi_override} 

56 

57 

58_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!*+./?@_' 

59def _validate_char(char): 

60 orig_char = char 

61 if char[0] == '-' and len(char) > 1: 

62 char = char[1:] 

63 if len(char) > 1: 

64 raise ValueError('char flags must be exactly one character, optionally' 

65 ' prefixed by a dash, not: %r' % orig_char) 

66 if char not in _VALID_CHARS: 

67 raise ValueError('expected valid flag character (ASCII letters, numbers,' 

68 ' or shell-compatible punctuation), not: %r' % orig_char) 

69 return char 

70 

71 

72def _posargs_to_provides(posargspec, posargs): 

73 '''Automatically unwrap injectable posargs into a more intuitive 

74 format, similar to an API a human might design. For instance, a 

75 function which takes exactly one argument would not take a list of 

76 exactly one argument. 

77 

78 Cases as follows: 

79 

80 1. min_count > 1 or max_count > 1, pass through posargs as a list 

81 2. max_count == 1 -> single argument or None 

82 

83 Even if min_count == 1, you can get a None back. This compromise 

84 was made necessary to keep "to_cmd_scope" robust enough to pass to 

85 help/error handler funcs when validation fails. 

86 ''' 

87 # all of the following assumes a valid posargspec, with min_count 

88 # <= max_count, etc. 

89 pas = posargspec 

90 if pas.max_count is None or pas.min_count > 1 or pas.max_count > 1: 

91 return posargs 

92 if pas.max_count == 1: 

93 # None is considered sufficiently unambiguous, even for cases when pas.min_count==1 

94 return posargs[0] if posargs else None 

95 raise RuntimeError('invalid posargspec/posargs configuration %r -- %r' 

96 % (posargspec, posargs)) # pragma: no cover (shouldn't get here) 

97 

98 

99class CommandParseResult: 

100 """The result of :meth:`Parser.parse`, instances of this type 

101 semantically store all that a command line can contain. Each 

102 argument corresponds 1:1 with an attribute. 

103 

104 Args: 

105 name (str): Top-level program name, typically the first 

106 argument on the command line, i.e., ``sys.argv[0]``. 

107 subcmds (tuple): Sequence of subcommand names. 

108 flags (OrderedDict): Mapping of canonical flag names to matched values. 

109 posargs (tuple): Sequence of parsed positional arguments. 

110 post_posargs (tuple): Sequence of parsed post-positional 

111 arguments (args following ``--``) 

112 parser (Parser): The Parser instance that parsed this 

113 result. Defaults to None. 

114 argv (tuple): The sequence of strings parsed by the Parser to 

115 yield this result. Defaults to ``()``. 

116 

117 Instances of this class can be injected by accepting the ``args_`` 

118 builtin in their Command handler function. 

119 

120 """ 

121 def __init__(self, parser, argv=()): 

122 self.parser = parser 

123 self.argv = tuple(argv) 

124 

125 self.name = None # str 

126 self.subcmds = None # tuple 

127 self.flags = None # OrderedDict 

128 self.posargs = None # tuple 

129 self.post_posargs = None # tuple 

130 

131 def to_cmd_scope(self): 

132 "returns a dict which can be used as kwargs in an inject call" 

133 _subparser = self.parser.subprs_map[self.subcmds] if self.subcmds else self.parser 

134 

135 if not self.argv: 

136 cmd_ = self.parser.name 

137 else: 

138 cmd_ = self.argv[0] 

139 path, basename = os.path.split(cmd_) 

140 if basename == '__main__.py': 

141 pkg_name = os.path.basename(path) 

142 executable_path = get_minimal_executable() 

143 cmd_ = f'{executable_path} -m {pkg_name}' 

144 else: 

145 cmd_ = get_minimal_executable(cmd_) 

146 

147 ret = {'args_': self, 

148 'cmd_': cmd_, 

149 'subcmds_': self.subcmds, 

150 'flags_': self.flags, 

151 'posargs_': self.posargs, 

152 'post_posargs_': self.post_posargs, 

153 'subcommand_': _subparser, 

154 'command_': self.parser} 

155 if self.flags: 

156 ret.update(self.flags) 

157 

158 prs = self.parser if not self.subcmds else self.parser.subprs_map[self.subcmds] 

159 if prs.posargs.provides: 

160 posargs_provides = _posargs_to_provides(prs.posargs, self.posargs) 

161 ret[prs.posargs.provides] = posargs_provides 

162 if prs.post_posargs.provides: 

163 posargs_provides = _posargs_to_provides(prs.posargs, self.post_posargs) 

164 ret[prs.post_posargs.provides] = posargs_provides 

165 

166 return ret 

167 

168 def __repr__(self): 

169 return format_nonexp_repr(self, ['name', 'argv', 'parser']) 

170 

171 

172# TODO: allow name="--flag / -F" and do the split for automatic 

173# char form? 

174class Flag: 

175 """The Flag object represents all there is to know about a resource 

176 that can be parsed from argv and consumed by a Command 

177 function. It also references a FlagDisplay, used by HelpHandlers 

178 to control formatting of the flag during --help output 

179 

180 Args: 

181 name (str): A string name for the flag, starting with a letter, 

182 and consisting of only ASCII letters, numbers, '-', and '_'. 

183 parse_as: How to interpret the flag. If *parse_as* is a 

184 callable, it will be called with the argument to the flag, 

185 the return value of which is stored in the parse result. If 

186 *parse_as* is not a callable, then the flag takes no 

187 argument, and the presence of the flag will produce this 

188 value in the parse result. Defaults to ``str``, meaning a 

189 default flag will take one string argument. 

190 missing: How to interpret the absence of the flag. Can be any 

191 value, which will be in the parse result when the flag is not 

192 present. Can also be the special value ``face.ERROR``, which 

193 will make the flag required. Defaults to ``None``. 

194 multi (str): How to handle multiple instances of the same 

195 flag. Pass 'overwrite' to accept the last flag's value. Pass 

196 'extend' to collect all values into a list. Pass 'error' to 

197 get the default behavior, which raises a DuplicateFlag 

198 exception. *multi* can also take a callable, which accepts a 

199 list of flag values and returns the value to be stored in the 

200 :class:`CommandParseResult`. 

201 char (str): A single-character short form for the flag. Can be 

202 user-friendly for commonly-used flags. Defaults to ``None``. 

203 doc (str): A summary of the flag's behavior, used in automatic 

204 help generation. 

205 display: Controls how the flag is displayed in automatic help 

206 generation. Pass False to hide the flag, pass a string to 

207 customize the label, and pass a FlagDisplay instance for full 

208 customizability. 

209 """ 

210 def __init__(self, name, parse_as=str, missing=None, multi='error', 

211 char=None, doc=None, display=None): 

212 self.name = flag_to_identifier(name) 

213 self.doc = doc 

214 self.parse_as = parse_as 

215 self.missing = missing 

216 if missing is ERROR and not callable(parse_as): 

217 raise ValueError('cannot make an argument-less flag required.' 

218 ' expected non-ERROR for missing, or a callable' 

219 ' for parse_as, not: %r' % parse_as) 

220 self.char = _validate_char(char) if char else None 

221 

222 if callable(multi): 

223 self.multi = multi 

224 elif multi in _MULTI_SHORTCUTS: 

225 self.multi = _MULTI_SHORTCUTS[multi] 

226 else: 

227 raise ValueError('multi expected callable, bool, or one of %r, not: %r' 

228 % (list(_MULTI_SHORTCUTS.keys()), multi)) 

229 

230 self.set_display(display) 

231 

232 def set_display(self, display): 

233 """Controls how the flag is displayed in automatic help 

234 generation. Pass False to hide the flag, pass a string to 

235 customize the label, and pass a FlagDisplay instance for full 

236 customizability. 

237 """ 

238 if display is None: 

239 display = {} 

240 elif isinstance(display, bool): 

241 display = {'hidden': not display} 

242 elif isinstance(display, str): 

243 display = {'label': display} 

244 if isinstance(display, dict): 

245 display = FlagDisplay(self, **display) 

246 if not isinstance(display, FlagDisplay): 

247 raise TypeError('expected bool, text name, dict of display' 

248 ' options, or FlagDisplay instance, not: %r' 

249 % display) 

250 self.display = display 

251 

252 def __repr__(self): 

253 return format_nonexp_repr(self, ['name', 'parse_as'], ['missing', 'multi'], 

254 opt_key=lambda v: v not in (None, _multi_error)) 

255 

256 

257class FlagDisplay: 

258 """Provides individual overrides for most of a given flag's display 

259 settings, as used by HelpFormatter instances attached to Parser 

260 and Command objects. Pass an instance of this to 

261 Flag.set_display() for full control of help output. 

262 

263 FlagDisplay instances are meant to be used 1:1 with Flag 

264 instances, as they maintain a reference back to their associated 

265 Flag. They are generally automatically created by a Flag 

266 constructor, based on the "display" argument. 

267 

268 Args: 

269 flag (Flag): The Flag instance to which this FlagDisplay applies. 

270 label (str): The formatted version of the string used to 

271 represent the flag in help and error messages. Defaults to 

272 None, which allows the label to be autogenerated by the 

273 HelpFormatter. 

274 post_doc (str): An addendum string added to the Flag's own 

275 doc. Defaults to a parenthetical describing whether the flag 

276 takes an argument, and whether the argument is required. 

277 full_doc (str): A string of the whole flag's doc, overriding 

278 the doc + post_doc default. 

279 value_name (str): For flags which take an argument, the string 

280 to use as the placeholder of the flag argument in help and 

281 error labels. 

282 hidden (bool): Pass True to hide this flag in general help and 

283 error messages. Defaults to False. 

284 group: An integer or string indicating how this flag should be 

285 grouped in help messages, improving readability. Integers are 

286 unnamed groups, strings are for named groups. Defaults to 0. 

287 sort_key: Flags are sorted in help output, pass an integer or 

288 string to override the sort order. 

289 

290 """ 

291 # value_name -> arg_name? 

292 def __init__(self, flag, *, 

293 label: Optional[str] = None, 

294 post_doc: Optional[str] = None, 

295 full_doc: Optional[str] = None, 

296 value_name: Optional[str] = None, 

297 group: int = 0, 

298 hidden: bool = False, 

299 sort_key: int = 0): 

300 self.flag = flag 

301 

302 self.doc = flag.doc 

303 if self.doc is None and callable(flag.parse_as): 

304 _prep, desc = get_type_desc(flag.parse_as) 

305 self.doc = 'Parsed with ' + desc 

306 if _prep == 'as': 

307 self.doc = desc 

308 

309 self.post_doc = post_doc 

310 self.full_doc = full_doc 

311 

312 self.value_name = '' 

313 if callable(flag.parse_as): 

314 # TODO: use default when it's set and it's a basic renderable type 

315 self.value_name = value_name or self.flag.name.upper() 

316 

317 self.group = group 

318 self._hide = hidden 

319 self.label = label # see hidden property below for more info 

320 self.sort_key = sort_key 

321 # TODO: sort_key is gonna need to be partitioned on type for py3 

322 # TODO: maybe sort_key should be a counter so that flags sort 

323 # in the order they are created 

324 return 

325 

326 @property 

327 def hidden(self): 

328 return self._hide or self.label == '' 

329 

330 def __repr__(self): 

331 return format_nonexp_repr(self, ['label', 'doc'], ['group', 'hidden'], opt_key=bool) 

332 

333 

334class PosArgDisplay: 

335 """Provides individual overrides for PosArgSpec display in automated 

336 help formatting. Pass to a PosArgSpec constructor, which is in 

337 turn passed to a Command/Parser. 

338 

339 Args: 

340 spec (PosArgSpec): The associated PosArgSpec. 

341 name (str): The string name of an individual positional 

342 argument. Automatically pluralized in the label according to 

343 PosArgSpec values. Defaults to 'arg'. 

344 label (str): The full display label for positional arguments, 

345 bypassing the automatic formatting of the *name* parameter. 

346 doc (str): A summary description of the positional arguments. 

347 post_doc (str): An informational addendum about the arguments, 

348 often describes default behavior. 

349 

350 """ 

351 def __init__(self, *, 

352 name: Optional[str] = None, 

353 doc: str = '', 

354 post_doc: Optional[str] = None, 

355 hidden: bool = False, 

356 label: Optional[str] = None) -> None: 

357 self.name = name or 'arg' 

358 self.doc = doc 

359 self.post_doc = post_doc 

360 self._hide = hidden 

361 self.label = label 

362 

363 @property 

364 def hidden(self): 

365 return self._hide or self.label == '' 

366 

367 def __repr__(self): 

368 return format_nonexp_repr(self, ['name', 'label']) 

369 

370 

371class PosArgSpec: 

372 """Passed to Command/Parser as posargs and post_posargs parameters to 

373 configure the number and type of positional arguments. 

374 

375 Args: 

376 parse_as (callable): A function to call on each of the passed 

377 arguments. Also accepts special argument ERROR, which will raise 

378 an exception if positional arguments are passed. Defaults to str. 

379 min_count (int): A minimimum number of positional 

380 arguments. Defaults to 0. 

381 max_count (int): A maximum number of positional arguments. Also 

382 accepts None, meaning no maximum. Defaults to None. 

383 display: Pass a string to customize the name in help output, or 

384 False to hide it completely. Also accepts a PosArgDisplay 

385 instance, or a dict of the respective arguments. 

386 provides (str): name of an argument to be passed to a receptive 

387 handler function. 

388 name (str): A shortcut to set *display* name and *provides* 

389 count (int): A shortcut to set min_count and max_count to a single value 

390 when an exact number of arguments should be specified. 

391 

392 PosArgSpec instances are stateless and safe to be used multiple 

393 times around the application. 

394 

395 """ 

396 def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None, 

397 *, name: Optional[str] = None, count: Optional[int] = None): 

398 if not callable(parse_as) and parse_as is not ERROR: 

399 raise TypeError(f'expected callable or ERROR for parse_as, not {parse_as!r}') 

400 

401 self.parse_as = parse_as 

402 

403 # count convenience alias 

404 min_count = count if min_count is None else min_count 

405 max_count = count if max_count is None else max_count 

406 

407 self.min_count = int(min_count) if min_count else 0 

408 self.max_count = int(max_count) if max_count is not None else None 

409 

410 if self.min_count < 0: 

411 raise ValueError(f'expected min_count >= 0, not: {self.min_count!r}') 

412 if self.max_count is not None and self.max_count <= 0: 

413 raise ValueError(f'expected max_count > 0, not: {self.max_count!r}') 

414 if self.max_count and self.min_count > self.max_count: 

415 raise ValueError('expected min_count > max_count, not: %r > %r' 

416 % (self.min_count, self.max_count)) 

417 

418 provides = name if provides is None else provides 

419 self.provides = provides 

420 

421 if display is None: 

422 display = {} 

423 elif isinstance(display, bool): 

424 display = {'hidden': not display} 

425 elif isinstance(display, str): 

426 display = {'name': display} 

427 if isinstance(display, dict): 

428 display.setdefault('name', name) 

429 display = PosArgDisplay(**display) 

430 if not isinstance(display, PosArgDisplay): 

431 raise TypeError('expected bool, text name, dict of display' 

432 ' options, or PosArgDisplay instance, not: %r' 

433 % display) 

434 

435 self.display = display 

436 

437 # TODO: default? type check that it's a sequence matching min/max reqs 

438 

439 def __repr__(self): 

440 return format_nonexp_repr(self, ['parse_as', 'min_count', 'max_count', 'display']) 

441 

442 @property 

443 def accepts_args(self): 

444 """True if this PosArgSpec is configured to accept one or 

445 more arguments. 

446 """ 

447 return self.parse_as is not ERROR 

448 

449 def parse(self, posargs): 

450 """Parse a list of strings as positional arguments. 

451 

452 Args: 

453 posargs (list): List of strings, likely parsed by a Parser 

454 instance from sys.argv. 

455 

456 Raises an ArgumentArityError if there are too many or too few 

457 arguments. 

458 

459 Raises InvalidPositionalArgument if the argument doesn't match 

460 the configured *parse_as*. See PosArgSpec for more info. 

461 

462 Returns a list of arguments, parsed with *parse_as*. 

463 """ 

464 len_posargs = len(posargs) 

465 if posargs and not self.accepts_args: 

466 # TODO: check for likely subcommands 

467 raise ArgumentArityError(f'unexpected positional arguments: {posargs!r}') 

468 min_count, max_count = self.min_count, self.max_count 

469 if min_count == max_count: 

470 # min_count must be >0 because max_count cannot be 0 

471 arg_range_text = f'{min_count} argument' 

472 if min_count > 1: 

473 arg_range_text += 's' 

474 else: 

475 if min_count == 0: 

476 arg_range_text = f'up to {max_count} argument' 

477 arg_range_text += 's' if (max_count and max_count > 1) else '' 

478 elif max_count is None: 

479 arg_range_text = f'at least {min_count} argument' 

480 arg_range_text += 's' if min_count > 1 else '' 

481 else: 

482 arg_range_text = f'{min_count} - {max_count} arguments' 

483 

484 if len_posargs < min_count: 

485 raise ArgumentArityError('too few arguments, expected %s, got %s' 

486 % (arg_range_text, len_posargs)) 

487 if max_count is not None and len_posargs > max_count: 

488 raise ArgumentArityError('too many arguments, expected %s, got %s' 

489 % (arg_range_text, len_posargs)) 

490 ret = [] 

491 for pa in posargs: 

492 try: 

493 val = self.parse_as(pa) 

494 except Exception as exc: 

495 raise InvalidPositionalArgument.from_parse(self, pa, exc) 

496 else: 

497 ret.append(val) 

498 return ret 

499 

500 

501FLAGFILE_ENABLED = Flag('--flagfile', parse_as=str, multi='extend', missing=None, display=False, doc='') 

502 

503 

504def _ensure_posargspec(posargs, posargs_name): 

505 if not posargs: 

506 # take no posargs 

507 posargs = PosArgSpec(parse_as=ERROR) 

508 elif posargs is True: 

509 # take any number of posargs 

510 posargs = PosArgSpec() 

511 elif isinstance(posargs, int): 

512 # take an exact number of posargs 

513 # (True and False are handled above, so only real nonzero ints get here) 

514 posargs = PosArgSpec(min_count=posargs, max_count=posargs) 

515 elif isinstance(posargs, str): 

516 posargs = PosArgSpec(display=posargs, provides=posargs) 

517 elif isinstance(posargs, dict): 

518 posargs = PosArgSpec(**posargs) 

519 elif callable(posargs): 

520 # take any number of posargs of a given format 

521 posargs = PosArgSpec(parse_as=posargs) 

522 

523 if not isinstance(posargs, PosArgSpec): 

524 raise TypeError('expected %s as True, False, number of args, text name of args,' 

525 ' dict of PosArgSpec options, or instance of PosArgSpec, not: %r' 

526 % (posargs_name, posargs)) 

527 

528 return posargs 

529 

530 

531class Parser: 

532 """The Parser lies at the center of face, primarily providing a 

533 configurable validation logic on top of the conventional grammar 

534 for CLI argument parsing. 

535 

536 Args: 

537 name (str): A name used to identify this command. Important 

538 when the command is embedded as a subcommand of another 

539 command. 

540 doc (str): An optional summary description of the command, used 

541 to generate help and usage information. 

542 flags (list): A list of Flag instances. Optional, as flags can 

543 be added with :meth:`~Parser.add()`. 

544 posargs (bool): Defaults to disabled, pass ``True`` to enable 

545 the Parser to accept positional arguments. Pass a callable 

546 to parse the positional arguments using that 

547 function/type. Pass a :class:`PosArgSpec` for full 

548 customizability. 

549 post_posargs (bool): Same as *posargs*, but refers to the list 

550 of arguments following the ``--`` conventional marker. See 

551 ``git`` and ``tox`` for examples of commands using this 

552 style of positional argument. 

553 flagfile (bool): Defaults to enabled, pass ``False`` to disable 

554 flagfile support. Pass a :class:`Flag` instance to use a 

555 custom flag instead of ``--flagfile``. Read more about 

556 Flagfiles below. 

557 

558 Once initialized, parsing is performed by calling 

559 :meth:`Parser.parse()` with ``sys.argv`` or any other list of strings. 

560 """ 

561 def __init__(self, name, doc=None, flags=None, posargs=None, 

562 post_posargs=None, flagfile=True, group=None): 

563 self.name = process_command_name(name) 

564 self.doc = doc 

565 self.group = group 

566 flags = list(flags or []) 

567 

568 self.posargs = _ensure_posargspec(posargs, 'posargs') 

569 self.post_posargs = _ensure_posargspec(post_posargs, 'post_posargs') 

570 

571 if flagfile is True: 

572 self.flagfile_flag = FLAGFILE_ENABLED 

573 elif isinstance(flagfile, Flag): 

574 self.flagfile_flag = flagfile 

575 elif not flagfile: 

576 self.flagfile_flag = None 

577 else: 

578 raise TypeError('expected True, False, or Flag instance for' 

579 ' flagfile, not: %r' % flagfile) 

580 

581 self.subprs_map = OrderedDict() 

582 self._path_flag_map = OrderedDict() 

583 self._path_flag_map[()] = OrderedDict() 

584 

585 for flag in flags: 

586 self.add(flag) 

587 if self.flagfile_flag: 

588 self.add(self.flagfile_flag) 

589 return 

590 

591 def get_flag_map(self, path, with_hidden=True): 

592 flag_map = self._path_flag_map[path] 

593 return OrderedDict([(k, f) for k, f in flag_map.items() 

594 if with_hidden or not f.display.hidden]) 

595 

596 def get_flags(self, path=(), with_hidden=True): 

597 flag_map = self.get_flag_map(path=path, with_hidden=with_hidden) 

598 

599 return unique(flag_map.values()) 

600 

601 def __repr__(self): 

602 cn = self.__class__.__name__ 

603 return ('<%s name=%r subcmd_count=%r flag_count=%r posargs=%r>' 

604 % (cn, self.name, len(self.subprs_map), len(self.get_flags()), self.posargs)) 

605 

606 def _add_subparser(self, subprs): 

607 """Process subcommand name, check for subcommand conflicts, check for 

608 subcommand flag conflicts, then finally add subcommand. 

609 

610 To add a command under a different name, simply make a copy of 

611 that parser or command with a different name. 

612 """ 

613 if self.posargs.accepts_args: 

614 raise ValueError('commands accepting positional arguments' 

615 ' cannot take subcommands') 

616 

617 # validate that the subparser's name can be used as a subcommand 

618 subprs_name = process_command_name(subprs.name) 

619 

620 # then, check for conflicts with existing subcommands and flags 

621 for prs_path in self.subprs_map: 

622 if prs_path[0] == subprs_name: 

623 raise ValueError(f'conflicting subcommand name: {subprs_name!r}') 

624 parent_flag_map = self._path_flag_map[()] 

625 

626 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True 

627 for path, flags in subprs._path_flag_map.items(): 

628 if not check_no_conflicts(parent_flag_map, path, flags): 

629 # TODO 

630 raise ValueError(f'subcommand flags conflict with parent command: {flags!r}') 

631 

632 # with checks complete, add parser and all subparsers 

633 self.subprs_map[(subprs_name,)] = subprs 

634 for path, cur_subprs in list(subprs.subprs_map.items()): 

635 new_path = (subprs_name,) + path 

636 self.subprs_map[new_path] = cur_subprs 

637 

638 # Flags inherit down (a parent's flags are usable by the child) 

639 for path, flags in subprs._path_flag_map.items(): 

640 new_flags = parent_flag_map.copy() 

641 new_flags.update(flags) 

642 self._path_flag_map[(subprs_name,) + path] = new_flags 

643 

644 # If two flags have the same name, as long as the "parse_as" 

645 # is the same, things should be ok. Need to watch for 

646 # overlapping aliases, too. This may allow subcommands to 

647 # further document help strings. Should the same be allowed 

648 # for defaults? 

649 

650 def add(self, *a, **kw): 

651 """Add a flag or subparser. 

652 

653 Unless the first argument is a Parser or Flag object, the 

654 arguments are the same as the Flag constructor, and will be 

655 used to create a new Flag instance to be added. 

656 

657 May raise ValueError if arguments are not recognized as 

658 Parser, Flag, or Flag parameters. ValueError may also be 

659 raised on duplicate definitions and other conflicts. 

660 """ 

661 if isinstance(a[0], Parser): 

662 subprs = a[0] 

663 self._add_subparser(subprs) 

664 return 

665 

666 if isinstance(a[0], Flag): 

667 flag = a[0] 

668 else: 

669 try: 

670 flag = Flag(*a, **kw) 

671 except TypeError as te: 

672 raise ValueError('expected Parser, Flag, or Flag parameters,' 

673 ' not: %r, %r (got %r)' % (a, kw, te)) 

674 return self._add_flag(flag) 

675 

676 def _add_flag(self, flag): 

677 # first check there are no conflicts... 

678 for subcmds, flag_map in self._path_flag_map.items(): 

679 conflict_flag = flag_map.get(flag.name) or (flag.char and flag_map.get(flag.char)) 

680 if conflict_flag is None: 

681 continue 

682 if flag.name in (conflict_flag.name, conflict_flag.char): 

683 raise ValueError('pre-existing flag %r conflicts with name of new flag %r' 

684 % (conflict_flag, flag.name)) 

685 if flag.char and flag.char in (conflict_flag.name, conflict_flag.char): 

686 raise ValueError('pre-existing flag %r conflicts with short form for new flag %r' 

687 % (conflict_flag, flag)) 

688 

689 # ... then we add the flags 

690 for flag_map in self._path_flag_map.values(): 

691 flag_map[flag.name] = flag 

692 if flag.char: 

693 flag_map[flag.char] = flag 

694 return 

695 

696 def parse(self, argv): 

697 """This method takes a list of strings and converts them into a 

698 validated :class:`CommandParseResult` according to the flags, 

699 subparsers, and other options configured. 

700 

701 Args: 

702 argv (list): A required list of strings. Pass ``None`` to 

703 use ``sys.argv``. 

704 

705 This method may raise ArgumentParseError (or one of its 

706 subtypes) if the list of strings fails to parse. 

707 

708 .. note:: The *argv* parameter does not automatically default 

709 to using ``sys.argv`` because it's best practice for 

710 implementing codebases to perform that sort of 

711 defaulting in their ``main()``, which should accept 

712 an ``argv=None`` parameter. This simple step ensures 

713 that the Python CLI application has some sort of 

714 programmatic interface that doesn't require 

715 subprocessing. See here for an example. 

716 

717 """ 

718 if argv is None: 

719 argv = sys.argv 

720 cpr = CommandParseResult(parser=self, argv=argv) 

721 if not argv: 

722 ape = ArgumentParseError(f'expected non-empty sequence of arguments, not: {argv!r}') 

723 ape.prs_res = cpr 

724 raise ape 

725 for arg in argv: 

726 if not isinstance(arg, str): 

727 raise TypeError(f'parse expected all args as strings, not: {arg!r} ({type(arg).__name__})') 

728 ''' 

729 for subprs_path, subprs in self.subprs_map.items(): 

730 if len(subprs_path) == 1: 

731 # _add_subparser takes care of recurring so we only 

732 # need direct subparser descendants 

733 self._add_subparser(subprs, overwrite=True) 

734 ''' 

735 flag_map = None 

736 # first snip off the first argument, the command itself 

737 cmd_name, args = argv[0], list(argv)[1:] 

738 cpr.name = cmd_name 

739 

740 # we record our progress as we parse to provide the most 

741 # up-to-date info possible to the error and help handlers 

742 

743 try: 

744 # then figure out the subcommand path 

745 subcmds, args = self._parse_subcmds(args) 

746 cpr.subcmds = tuple(subcmds) 

747 

748 prs = self.subprs_map[tuple(subcmds)] if subcmds else self 

749 

750 # then look up the subcommand's supported flags 

751 # NOTE: get_flag_map() is used so that inheritors, like Command, 

752 # can filter by actually-used arguments, not just 

753 # available arguments. 

754 cmd_flag_map = self.get_flag_map(path=tuple(subcmds)) 

755 

756 # parse supported flags and validate their arguments 

757 flag_map, flagfile_map, posargs = self._parse_flags(cmd_flag_map, args) 

758 cpr.flags = OrderedDict(flag_map) 

759 cpr.posargs = tuple(posargs) 

760 

761 # take care of dupes and check required flags 

762 resolved_flag_map = self._resolve_flags(cmd_flag_map, flag_map, flagfile_map) 

763 cpr.flags = OrderedDict(resolved_flag_map) 

764 

765 # separate out any trailing arguments from normal positional arguments 

766 post_posargs = None # TODO: default to empty list? 

767 parsed_post_posargs = None 

768 if '--' in posargs: 

769 posargs, post_posargs = split(posargs, '--', 1) 

770 cpr.posargs, cpr.post_posargs = posargs, post_posargs 

771 

772 parsed_post_posargs = prs.post_posargs.parse(post_posargs) 

773 cpr.post_posargs = tuple(parsed_post_posargs) 

774 

775 parsed_posargs = prs.posargs.parse(posargs) 

776 cpr.posargs = tuple(parsed_posargs) 

777 except ArgumentParseError as ape: 

778 ape.prs_res = cpr 

779 raise 

780 

781 return cpr 

782 

783 def _parse_subcmds(self, args): 

784 """Expects arguments after the initial command (i.e., argv[1:]) 

785 

786 Returns a tuple of (list_of_subcmds, remaining_args). 

787 

788 Raises on unknown subcommands.""" 

789 ret = [] 

790 

791 for arg in args: 

792 if arg.startswith('-'): 

793 break # subcmd parsing complete 

794 

795 arg = _arg_to_subcmd(arg) 

796 if tuple(ret + [arg]) not in self.subprs_map: 

797 prs = self.subprs_map[tuple(ret)] if ret else self 

798 if prs.posargs.parse_as is not ERROR or not prs.subprs_map: 

799 # we actually have posargs from here 

800 break 

801 raise InvalidSubcommand.from_parse(prs, arg) 

802 ret.append(arg) 

803 return ret, args[len(ret):] 

804 

805 def _parse_single_flag(self, cmd_flag_map, args): 

806 advance = 1 

807 arg = args[0] 

808 arg_text = None 

809 try: 

810 arg, arg_text = arg.split('=', maxsplit=1) 

811 except ValueError: 

812 pass 

813 flag = cmd_flag_map.get(normalize_flag_name(arg)) 

814 if flag is None: 

815 raise UnknownFlag.from_parse(cmd_flag_map, arg) 

816 parse_as = flag.parse_as 

817 if not callable(parse_as): 

818 if arg_text: 

819 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text) 

820 # e.g., True is effectively store_true, False is effectively store_false 

821 return flag, parse_as, args[1:] 

822 

823 try: 

824 if arg_text is None: 

825 arg_text = args[1] 

826 advance = 2 

827 except IndexError: 

828 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg=None) 

829 try: 

830 arg_val = parse_as(arg_text) 

831 except Exception as e: 

832 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text, exc=e) 

833 

834 return flag, arg_val, args[advance:] 

835 

836 def _parse_flags(self, cmd_flag_map, args): 

837 """Expects arguments after the initial command and subcommands (i.e., 

838 the second item returned from _parse_subcmds) 

839 

840 Returns a tuple of (multidict of flag names to parsed and validated values, remaining_args). 

841 

842 Raises on unknown subcommands. 

843 """ 

844 flag_value_map = OMD() 

845 ff_path_res_map = OrderedDict() 

846 ff_path_seen = set() 

847 

848 orig_args = args 

849 while args: 

850 arg = args[0] 

851 if not arg or arg[0] != '-' or arg == '-' or arg == '--': 

852 # posargs or post_posargs beginning ('-' is a conventional pos arg for stdin) 

853 break 

854 flag, value, args = self._parse_single_flag(cmd_flag_map, args) 

855 flag_value_map.add(flag.name, value) 

856 

857 if flag is self.flagfile_flag: 

858 self._parse_flagfile(cmd_flag_map, value, res_map=ff_path_res_map) 

859 for path, ff_flag_value_map in ff_path_res_map.items(): 

860 if path in ff_path_seen: 

861 continue 

862 flag_value_map.update_extend(ff_flag_value_map) 

863 ff_path_seen.add(path) 

864 

865 return flag_value_map, ff_path_res_map, args 

866 

867 def _parse_flagfile(self, cmd_flag_map, path_or_file, res_map=None): 

868 ret = res_map if res_map is not None else OrderedDict() 

869 if callable(getattr(path_or_file, 'read', None)): 

870 # enable StringIO and custom flagfile opening 

871 f_name = getattr(path_or_file, 'name', None) 

872 path = os.path.abspath(f_name) if f_name else repr(path_or_file) 

873 ff_text = path_or_file.read() 

874 else: 

875 path = os.path.abspath(path_or_file) 

876 try: 

877 with open(path_or_file, 'r', encoding='utf-8') as f: 

878 ff_text = f.read() 

879 except (UnicodeError, OSError) as ee: 

880 raise ArgumentParseError(f'failed to load flagfile "{path}", got: {ee!r}') 

881 if path in res_map: 

882 # we've already seen this file 

883 return res_map 

884 ret[path] = cur_file_res = OMD() 

885 lines = ff_text.splitlines() 

886 for lineno, line in enumerate(lines, 1): 

887 try: 

888 args = shlex.split(line, comments=True) 

889 if not args: 

890 continue # comment or empty line 

891 flag, value, leftover_args = self._parse_single_flag(cmd_flag_map, args) 

892 

893 if leftover_args: 

894 raise ArgumentParseError('excessive flags or arguments for flag "%s",' 

895 ' expected one flag per line' % flag.name) 

896 

897 cur_file_res.add(flag.name, value) 

898 if flag is self.flagfile_flag: 

899 self._parse_flagfile(cmd_flag_map, value, res_map=ret) 

900 

901 except FaceException as fe: 

902 fe.args = (fe.args[0] + f' (on line {lineno} of flagfile "{path}")',) 

903 raise 

904 

905 return ret 

906 

907 def _resolve_flags(self, cmd_flag_map, parsed_flag_map, flagfile_map=None): 

908 ret = OrderedDict() 

909 cfm, pfm = cmd_flag_map, parsed_flag_map 

910 flagfile_map = flagfile_map or {} 

911 

912 # check requireds and set defaults and then... 

913 missing_flags = [] 

914 for flag_name, flag in cfm.items(): 

915 if flag.name in pfm: 

916 continue 

917 if flag.missing is ERROR: 

918 missing_flags.append(flag.name) 

919 else: 

920 pfm[flag.name] = flag.missing 

921 if missing_flags: 

922 raise MissingRequiredFlags.from_parse(cfm, pfm, missing_flags) 

923 

924 # ... resolve dupes 

925 for flag_name in pfm: 

926 flag = cfm[flag_name] 

927 arg_val_list = pfm.getlist(flag_name) 

928 try: 

929 ret[flag_name] = flag.multi(flag, arg_val_list) 

930 except FaceException as fe: 

931 ff_paths = [] 

932 for ff_path, ff_value_map in flagfile_map.items(): 

933 if flag_name in ff_value_map: 

934 ff_paths.append(ff_path) 

935 if ff_paths: 

936 ff_label = 'flagfiles' if len(ff_paths) > 1 else 'flagfile' 

937 msg = ('\n\t(check %s with definitions for flag "%s": %s)' 

938 % (ff_label, flag_name, ', '.join(ff_paths))) 

939 fe.args = (fe.args[0] + msg,) 

940 raise 

941 return ret 

942 

943 

944def parse_sv_line(line, sep=','): 

945 """Parse a single line of values, separated by the delimiter 

946 *sep*. Supports quoting. 

947 

948 """ 

949 # TODO: this doesn't support unicode, which is intended to be 

950 # handled at the layer above. 

951 from csv import reader, Dialect, QUOTE_MINIMAL 

952 

953 class _face_dialect(Dialect): 

954 delimiter = sep 

955 escapechar = '\\' 

956 quotechar = '"' 

957 doublequote = True 

958 skipinitialspace = False 

959 lineterminator = '\n' 

960 quoting = QUOTE_MINIMAL 

961 

962 parsed = list(reader([line], dialect=_face_dialect)) 

963 return parsed[0] 

964 

965 

966class ListParam: 

967 """The ListParam takes an argument as a character-separated list, and 

968 produces a Python list of parsed values. Basically, the argument 

969 equivalent of CSV (Comma-Separated Values):: 

970 

971 --flag a1,b2,c3 

972 

973 By default, this yields a ``['a1', 'b2', 'c3']`` as the value for 

974 ``flag``. The format is also similar to CSV in that it supports 

975 quoting when values themselves contain the separator:: 

976 

977 --flag 'a1,"b,2",c3' 

978 

979 Args: 

980 parse_one_as (callable): Turns a single value's text into its 

981 parsed value. 

982 sep (str): A single-character string representing the list 

983 value separator. Defaults to ``,``. 

984 strip (bool): Whether or not each value in the list should have 

985 whitespace stripped before being passed to 

986 *parse_one_as*. Defaults to False. 

987 

988 .. note:: Aside from using ListParam, an alternative method for 

989 accepting multiple arguments is to use the 

990 ``multi=True`` on the :class:`Flag` constructor. The 

991 approach tends to be more verbose and can be confusing 

992 because arguments can get spread across the command 

993 line. 

994 

995 """ 

996 def __init__(self, parse_one_as=str, sep=',', strip=False): 

997 # TODO: min/max limits? 

998 self.parse_one_as = parse_one_as 

999 self.sep = sep 

1000 self.strip = strip 

1001 

1002 def parse(self, list_text): 

1003 "Parse a single string argument into a list of arguments." 

1004 split_vals = parse_sv_line(list_text, self.sep) 

1005 if self.strip: 

1006 split_vals = [v.strip() for v in split_vals] 

1007 return [self.parse_one_as(v) for v in split_vals] 

1008 

1009 __call__ = parse 

1010 

1011 def __repr__(self): 

1012 return format_exp_repr(self, ['parse_one_as'], ['sep', 'strip']) 

1013 

1014 

1015class ChoicesParam: 

1016 """Parses a single value, limited to a set of *choices*. The actual 

1017 converter used to parse is inferred from *choices* by default, but 

1018 an explicit one can be set *parse_as*. 

1019 """ 

1020 def __init__(self, choices, parse_as=None): 

1021 if not choices: 

1022 raise ValueError(f'expected at least one choice, not: {choices!r}') 

1023 try: 

1024 self.choices = sorted(choices) 

1025 except Exception: 

1026 # in case choices aren't sortable 

1027 self.choices = list(choices) 

1028 if parse_as is None: 

1029 parse_as = type(self.choices[0]) 

1030 # TODO: check for builtins, raise if not a supported type 

1031 self.parse_as = parse_as 

1032 

1033 def parse(self, text): 

1034 choice = self.parse_as(text) 

1035 if choice not in self.choices: 

1036 raise ArgumentParseError(f'expected one of {self.choices!r}, not: {text!r}') 

1037 return choice 

1038 

1039 __call__ = parse 

1040 

1041 def __repr__(self): 

1042 return format_exp_repr(self, ['choices'], ['parse_as']) 

1043 

1044 

1045class FilePathParam: 

1046 """TODO 

1047 

1048 ideas: exists, minimum permissions, can create, abspath, type=d/f 

1049 (technically could also support socket, named pipe, and symlink) 

1050 

1051 could do missing=TEMP, but that might be getting too fancy tbh. 

1052 """ 

1053 

1054class FileValueParam: 

1055 """ 

1056 TODO: file with a single value in it, like a pidfile 

1057 or a password file mounted in. Read in and treated like it 

1058 was on the argv. 

1059 """