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

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

497 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 # 'overwrite' was an 8-year docstring typo for 

57 # 'override'; accepted as a quiet alias, but never 

58 # advertised in docs, help, or error messages. 

59 'overwrite': _multi_override} 

60 

61_MULTI_SHORTCUT_NAMES = ('error', 'extend', 'override') 

62 

63 

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

65def _validate_char(char): 

66 orig_char = char 

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

68 char = char[1:] 

69 if len(char) > 1: 

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

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

72 if char not in _VALID_CHARS: 

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

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

75 return char 

76 

77 

78def _posargs_to_provides(posargspec, posargs): 

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

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

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

82 exactly one argument. 

83 

84 Cases as follows: 

85 

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

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

88 

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

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

91 help/error handler funcs when validation fails. 

92 ''' 

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

94 # <= max_count, etc. 

95 pas = posargspec 

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

97 return posargs 

98 if pas.max_count == 1: 

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

100 return posargs[0] if posargs else None 

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

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

103 

104 

105class CommandParseResult: 

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

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

108 argument corresponds 1:1 with an attribute. 

109 

110 Args: 

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

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

113 subcmds (tuple): Sequence of subcommand names. 

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

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

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

117 arguments (args following ``--``) 

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

119 result. Defaults to None. 

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

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

122 

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

124 builtin in their Command handler function. 

125 

126 """ 

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

128 self.parser = parser 

129 self.argv = tuple(argv) 

130 

131 self.name = None # str 

132 self.subcmds = None # tuple 

133 self.flags = None # OrderedDict 

134 self.posargs = None # tuple 

135 self.post_posargs = None # tuple 

136 

137 def to_cmd_scope(self): 

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

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

140 

141 if not self.argv: 

142 cmd_ = self.parser.name 

143 else: 

144 cmd_ = self.argv[0] 

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

146 if basename == '__main__.py': 

147 pkg_name = os.path.basename(path) 

148 executable_path = get_minimal_executable() 

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

150 else: 

151 cmd_ = get_minimal_executable(cmd_) 

152 

153 ret = {'args_': self, 

154 'cmd_': cmd_, 

155 'subcmds_': self.subcmds, 

156 'flags_': self.flags, 

157 'posargs_': self.posargs, 

158 'post_posargs_': self.post_posargs, 

159 'subcommand_': _subparser, 

160 'command_': self.parser} 

161 if self.flags: 

162 ret.update(self.flags) 

163 

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

165 if prs.posargs.provides: 

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

167 ret[prs.posargs.provides] = posargs_provides 

168 if prs.post_posargs.provides: 

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

170 ret[prs.post_posargs.provides] = posargs_provides 

171 

172 return ret 

173 

174 def __repr__(self): 

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

176 

177 

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

179# char form? 

180class Flag: 

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

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

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

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

185 

186 Args: 

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

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

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

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

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

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

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

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

195 default flag will take one string argument. 

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

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

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

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

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

201 flag. Pass 'override' to accept the last flag's value. Pass 

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

203 get the default behavior, which raises a DuplicateFlag 

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

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

206 :class:`CommandParseResult`. Note that 'extend' always 

207 produces a list: one element per occurrence, and an empty 

208 list when the flag is absent — *missing* is not delivered as 

209 a result value (though ``missing=ERROR`` still makes the 

210 flag required). 

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

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

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

214 help generation. 

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

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

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

218 customizability. 

219 """ 

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

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

222 self.name = flag_to_identifier(name) 

223 self.doc = doc 

224 self.parse_as = parse_as 

225 self.missing = missing 

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

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

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

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

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

231 

232 if callable(multi): 

233 self.multi = multi 

234 elif multi in _MULTI_SHORTCUTS: 

235 self.multi = _MULTI_SHORTCUTS[multi] 

236 else: 

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

238 % (list(_MULTI_SHORTCUT_NAMES), multi)) 

239 

240 self.set_display(display) 

241 

242 def set_display(self, display): 

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

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

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

246 customizability. 

247 """ 

248 if display is None: 

249 display = {} 

250 elif isinstance(display, bool): 

251 display = {'hidden': not display} 

252 elif isinstance(display, str): 

253 display = {'label': display} 

254 if isinstance(display, dict): 

255 display = FlagDisplay(self, **display) 

256 if not isinstance(display, FlagDisplay): 

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

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

259 % display) 

260 self.display = display 

261 

262 def __repr__(self): 

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

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

265 

266 

267class FlagDisplay: 

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

269 settings, as used by HelpFormatter instances attached to Parser 

270 and Command objects. Pass an instance of this to 

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

272 

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

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

275 Flag. They are generally automatically created by a Flag 

276 constructor, based on the "display" argument. 

277 

278 Args: 

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

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

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

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

283 HelpFormatter. 

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

285 doc. Defaults to a parenthetical describing whether the flag 

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

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

288 the doc + post_doc default. 

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

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

291 error labels. 

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

293 error messages. Defaults to False. 

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

295 grouped in help messages, improving readability. Integers are 

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

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

298 string to override the sort order. 

299 

300 """ 

301 # value_name -> arg_name? 

302 def __init__(self, flag, *, 

303 label: Optional[str] = None, 

304 post_doc: Optional[str] = None, 

305 full_doc: Optional[str] = None, 

306 value_name: Optional[str] = None, 

307 group: int = 0, 

308 hidden: bool = False, 

309 sort_key: int = 0): 

310 self.flag = flag 

311 

312 self.doc = flag.doc 

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

314 _prep, desc = get_type_desc(flag.parse_as) 

315 self.doc = 'Parsed with ' + desc 

316 if _prep == 'as': 

317 self.doc = desc 

318 

319 self.post_doc = post_doc 

320 self.full_doc = full_doc 

321 

322 self.value_name = '' 

323 if callable(flag.parse_as): 

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

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

326 

327 self.group = group 

328 self._hide = hidden 

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

330 self.sort_key = sort_key 

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

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

333 # in the order they are created 

334 return 

335 

336 @property 

337 def hidden(self): 

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

339 

340 def __repr__(self): 

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

342 

343 

344class PosArgDisplay: 

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

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

347 turn passed to a Command/Parser. 

348 

349 Args: 

350 spec (PosArgSpec): The associated PosArgSpec. 

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

352 argument. Automatically pluralized in the label according to 

353 PosArgSpec values. Defaults to 'arg'. 

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

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

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

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

358 often describes default behavior. 

359 

360 """ 

361 def __init__(self, *, 

362 name: Optional[str] = None, 

363 doc: str = '', 

364 post_doc: Optional[str] = None, 

365 hidden: bool = False, 

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

367 self.name = name or 'arg' 

368 self.doc = doc 

369 self.post_doc = post_doc 

370 self._hide = hidden 

371 self.label = label 

372 

373 @property 

374 def hidden(self): 

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

376 

377 def __repr__(self): 

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

379 

380 

381class PosArgSpec: 

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

383 configure the number and type of positional arguments. 

384 

385 Args: 

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

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

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

389 min_count (int): A minimimum number of positional 

390 arguments. Defaults to 0. 

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

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

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

394 False to hide it completely. Also accepts a PosArgDisplay 

395 instance, or a dict of the respective arguments. 

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

397 handler function. 

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

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

400 when an exact number of arguments should be specified. 

401 

402 PosArgSpec instances are stateless and safe to be used multiple 

403 times around the application. 

404 

405 """ 

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

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

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

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

410 

411 self.parse_as = parse_as 

412 

413 # count convenience alias 

414 min_count = count if min_count is None else min_count 

415 max_count = count if max_count is None else max_count 

416 

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

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

419 

420 if self.min_count < 0: 

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

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

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

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

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

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

427 

428 provides = name if provides is None else provides 

429 self.provides = provides 

430 

431 if display is None: 

432 display = {} 

433 elif isinstance(display, bool): 

434 display = {'hidden': not display} 

435 elif isinstance(display, str): 

436 display = {'name': display} 

437 if isinstance(display, dict): 

438 display.setdefault('name', name) 

439 display = PosArgDisplay(**display) 

440 if not isinstance(display, PosArgDisplay): 

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

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

443 % display) 

444 

445 self.display = display 

446 

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

448 

449 def __repr__(self): 

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

451 

452 @property 

453 def accepts_args(self): 

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

455 more arguments. 

456 """ 

457 return self.parse_as is not ERROR 

458 

459 def parse(self, posargs): 

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

461 

462 Args: 

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

464 instance from sys.argv. 

465 

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

467 arguments. 

468 

469 Raises InvalidPositionalArgument if the argument doesn't match 

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

471 

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

473 """ 

474 len_posargs = len(posargs) 

475 if posargs and not self.accepts_args: 

476 # TODO: check for likely subcommands 

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

478 min_count, max_count = self.min_count, self.max_count 

479 if min_count == max_count: 

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

481 arg_range_text = f'{min_count} argument' 

482 if min_count > 1: 

483 arg_range_text += 's' 

484 else: 

485 if min_count == 0: 

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

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

488 elif max_count is None: 

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

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

491 else: 

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

493 

494 if len_posargs < min_count: 

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

496 % (arg_range_text, len_posargs)) 

497 if max_count is not None and len_posargs > max_count: 

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

499 % (arg_range_text, len_posargs)) 

500 ret = [] 

501 for pa in posargs: 

502 try: 

503 val = self.parse_as(pa) 

504 except Exception as exc: 

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

506 else: 

507 ret.append(val) 

508 return ret 

509 

510 

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

512 

513 

514def _ensure_posargspec(posargs, posargs_name): 

515 if not posargs: 

516 # take no posargs 

517 posargs = PosArgSpec(parse_as=ERROR) 

518 elif posargs is True: 

519 # take any number of posargs 

520 posargs = PosArgSpec() 

521 elif isinstance(posargs, int): 

522 # take an exact number of posargs 

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

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

525 elif isinstance(posargs, str): 

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

527 elif isinstance(posargs, dict): 

528 posargs = PosArgSpec(**posargs) 

529 elif callable(posargs): 

530 # take any number of posargs of a given format 

531 posargs = PosArgSpec(parse_as=posargs) 

532 

533 if not isinstance(posargs, PosArgSpec): 

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

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

536 % (posargs_name, posargs)) 

537 

538 return posargs 

539 

540 

541class Parser: 

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

543 configurable validation logic on top of the conventional grammar 

544 for CLI argument parsing. 

545 

546 Args: 

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

548 when the command is embedded as a subcommand of another 

549 command. 

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

551 to generate help and usage information. 

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

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

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

555 the Parser to accept positional arguments. Pass a callable 

556 to parse the positional arguments using that 

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

558 customizability. 

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

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

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

562 style of positional argument. 

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

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

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

566 Flagfiles below. 

567 

568 Once initialized, parsing is performed by calling 

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

570 """ 

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

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

573 self.name = process_command_name(name) 

574 self.doc = doc 

575 self.group = group 

576 flags = list(flags or []) 

577 

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

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

580 

581 if flagfile is True: 

582 self.flagfile_flag = FLAGFILE_ENABLED 

583 elif isinstance(flagfile, Flag): 

584 self.flagfile_flag = flagfile 

585 elif not flagfile: 

586 self.flagfile_flag = None 

587 else: 

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

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

590 

591 self.subprs_map = OrderedDict() 

592 self._path_flag_map = OrderedDict() 

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

594 

595 for flag in flags: 

596 self.add(flag) 

597 if self.flagfile_flag: 

598 self.add(self.flagfile_flag) 

599 return 

600 

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

602 flag_map = self._path_flag_map[path] 

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

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

605 

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

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

608 

609 return unique(flag_map.values()) 

610 

611 def __repr__(self): 

612 cn = self.__class__.__name__ 

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

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

615 

616 def _add_subparser(self, subprs): 

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

618 subcommand flag conflicts, then finally add subcommand. 

619 

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

621 that parser or command with a different name. 

622 """ 

623 if self.posargs.accepts_args: 

624 raise ValueError('commands accepting positional arguments' 

625 ' cannot take subcommands') 

626 

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

628 subprs_name = process_command_name(subprs.name) 

629 

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

631 for prs_path in self.subprs_map: 

632 if prs_path[0] == subprs_name: 

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

634 parent_flag_map = self._path_flag_map[()] 

635 

636 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True 

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

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

639 # TODO 

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

641 

642 # with checks complete, add parser and all subparsers 

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

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

645 new_path = (subprs_name,) + path 

646 self.subprs_map[new_path] = cur_subprs 

647 

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

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

650 new_flags = parent_flag_map.copy() 

651 new_flags.update(flags) 

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

653 

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

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

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

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

658 # for defaults? 

659 

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

661 """Add a flag or subparser. 

662 

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

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

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

666 

667 May raise ValueError if arguments are not recognized as 

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

669 raised on duplicate definitions and other conflicts. 

670 """ 

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

672 subprs = a[0] 

673 self._add_subparser(subprs) 

674 return 

675 

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

677 flag = a[0] 

678 else: 

679 try: 

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

681 except TypeError as te: 

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

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

684 return self._add_flag(flag) 

685 

686 def _add_flag(self, flag): 

687 # first check there are no conflicts... 

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

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

690 if conflict_flag is None: 

691 continue 

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

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

694 % (conflict_flag, flag.name)) 

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

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

697 % (conflict_flag, flag)) 

698 

699 # ... then we add the flags 

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

701 flag_map[flag.name] = flag 

702 if flag.char: 

703 flag_map[flag.char] = flag 

704 return 

705 

706 def parse(self, argv): 

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

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

709 subparsers, and other options configured. 

710 

711 Args: 

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

713 use ``sys.argv``. 

714 

715 This method may raise ArgumentParseError (or one of its 

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

717 

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

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

720 implementing codebases to perform that sort of 

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

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

723 that the Python CLI application has some sort of 

724 programmatic interface that doesn't require 

725 subprocessing. See here for an example. 

726 

727 """ 

728 if argv is None: 

729 argv = sys.argv 

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

731 if not argv: 

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

733 ape.prs_res = cpr 

734 raise ape 

735 for arg in argv: 

736 if not isinstance(arg, str): 

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

738 ''' 

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

740 if len(subprs_path) == 1: 

741 # _add_subparser takes care of recurring so we only 

742 # need direct subparser descendants 

743 self._add_subparser(subprs, overwrite=True) 

744 ''' 

745 flag_map = None 

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

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

748 cpr.name = cmd_name 

749 

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

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

752 

753 try: 

754 # then figure out the subcommand path 

755 subcmds, args = self._parse_subcmds(args) 

756 cpr.subcmds = tuple(subcmds) 

757 

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

759 

760 # then look up the subcommand's supported flags 

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

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

763 # available arguments. 

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

765 

766 # parse supported flags and validate their arguments 

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

768 cpr.flags = OrderedDict(flag_map) 

769 cpr.posargs = tuple(posargs) 

770 

771 # take care of dupes and check required flags 

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

773 cpr.flags = OrderedDict(resolved_flag_map) 

774 

775 # separate out any trailing arguments from normal positional arguments 

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

777 parsed_post_posargs = None 

778 if '--' in posargs: 

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

780 cpr.posargs, cpr.post_posargs = posargs, post_posargs 

781 

782 parsed_post_posargs = prs.post_posargs.parse(post_posargs) 

783 cpr.post_posargs = tuple(parsed_post_posargs) 

784 

785 parsed_posargs = prs.posargs.parse(posargs) 

786 cpr.posargs = tuple(parsed_posargs) 

787 except ArgumentParseError as ape: 

788 ape.prs_res = cpr 

789 raise 

790 

791 return cpr 

792 

793 def _parse_subcmds(self, args): 

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

795 

796 Returns a tuple of (list_of_subcmds, remaining_args). 

797 

798 Raises on unknown subcommands.""" 

799 ret = [] 

800 

801 for arg in args: 

802 if arg.startswith('-'): 

803 break # subcmd parsing complete 

804 

805 arg = _arg_to_subcmd(arg) 

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

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

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

809 # we actually have posargs from here 

810 break 

811 raise InvalidSubcommand.from_parse(prs, arg) 

812 ret.append(arg) 

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

814 

815 def _parse_single_flag(self, cmd_flag_map, args): 

816 advance = 1 

817 arg = args[0] 

818 arg_text = None 

819 try: 

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

821 except ValueError: 

822 pass 

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

824 if flag is None: 

825 raise UnknownFlag.from_parse(cmd_flag_map, arg) 

826 parse_as = flag.parse_as 

827 if not callable(parse_as): 

828 if arg_text: 

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

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

831 return flag, parse_as, args[1:] 

832 

833 try: 

834 if arg_text is None: 

835 arg_text = args[1] 

836 advance = 2 

837 except IndexError: 

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

839 try: 

840 arg_val = parse_as(arg_text) 

841 except Exception as e: 

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

843 

844 return flag, arg_val, args[advance:] 

845 

846 def _parse_flags(self, cmd_flag_map, args): 

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

848 the second item returned from _parse_subcmds) 

849 

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

851 

852 Raises on unknown subcommands. 

853 """ 

854 flag_value_map = OMD() 

855 ff_path_res_map = OrderedDict() 

856 ff_path_seen = set() 

857 

858 orig_args = args 

859 while args: 

860 arg = args[0] 

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

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

863 break 

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

865 flag_value_map.add(flag.name, value) 

866 

867 if flag is self.flagfile_flag: 

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

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

870 if path in ff_path_seen: 

871 continue 

872 flag_value_map.update_extend(ff_flag_value_map) 

873 ff_path_seen.add(path) 

874 

875 return flag_value_map, ff_path_res_map, args 

876 

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

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

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

880 # enable StringIO and custom flagfile opening 

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

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

883 ff_text = path_or_file.read() 

884 else: 

885 path = os.path.abspath(path_or_file) 

886 try: 

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

888 ff_text = f.read() 

889 except (UnicodeError, OSError) as ee: 

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

891 if path in res_map: 

892 # we've already seen this file 

893 return res_map 

894 ret[path] = cur_file_res = OMD() 

895 lines = ff_text.splitlines() 

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

897 try: 

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

899 if not args: 

900 continue # comment or empty line 

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

902 

903 if leftover_args: 

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

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

906 

907 cur_file_res.add(flag.name, value) 

908 if flag is self.flagfile_flag: 

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

910 

911 except FaceException as fe: 

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

913 raise 

914 

915 return ret 

916 

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

918 ret = OrderedDict() 

919 cfm, pfm = cmd_flag_map, parsed_flag_map 

920 flagfile_map = flagfile_map or {} 

921 

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

923 missing_flags = [] 

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

925 if flag.name in pfm: 

926 continue 

927 if flag.missing is ERROR: 

928 missing_flags.append(flag.name) 

929 else: 

930 pfm[flag.name] = flag.missing 

931 if missing_flags: 

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

933 

934 # ... resolve dupes 

935 for flag_name in pfm: 

936 flag = cfm[flag_name] 

937 arg_val_list = pfm.getlist(flag_name) 

938 try: 

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

940 except FaceException as fe: 

941 ff_paths = [] 

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

943 if flag_name in ff_value_map: 

944 ff_paths.append(ff_path) 

945 if ff_paths: 

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

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

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

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

950 raise 

951 return ret 

952 

953 

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

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

956 *sep*. Supports quoting. 

957 

958 """ 

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

960 # handled at the layer above. 

961 from csv import reader, Dialect, QUOTE_MINIMAL 

962 

963 class _face_dialect(Dialect): 

964 delimiter = sep 

965 escapechar = '\\' 

966 quotechar = '"' 

967 doublequote = True 

968 skipinitialspace = False 

969 lineterminator = '\n' 

970 quoting = QUOTE_MINIMAL 

971 

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

973 return parsed[0] 

974 

975 

976class ListParam: 

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

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

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

980 

981 --flag a1,b2,c3 

982 

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

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

985 quoting when values themselves contain the separator:: 

986 

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

988 

989 Args: 

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

991 parsed value. 

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

993 value separator. Defaults to ``,``. 

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

995 whitespace stripped before being passed to 

996 *parse_one_as*. Defaults to False. 

997 

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

999 accepting multiple arguments is to use the 

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

1001 approach tends to be more verbose and can be confusing 

1002 because arguments can get spread across the command 

1003 line. 

1004 

1005 """ 

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

1007 # TODO: min/max limits? 

1008 self.parse_one_as = parse_one_as 

1009 self.sep = sep 

1010 self.strip = strip 

1011 

1012 def parse(self, list_text): 

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

1014 split_vals = parse_sv_line(list_text, self.sep) 

1015 if self.strip: 

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

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

1018 

1019 __call__ = parse 

1020 

1021 def __repr__(self): 

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

1023 

1024 

1025class ChoicesParam: 

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

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

1028 an explicit one can be set *parse_as*. 

1029 """ 

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

1031 if not choices: 

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

1033 try: 

1034 self.choices = sorted(choices) 

1035 except Exception: 

1036 # in case choices aren't sortable 

1037 self.choices = list(choices) 

1038 if parse_as is None: 

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

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

1041 self.parse_as = parse_as 

1042 

1043 def parse(self, text): 

1044 choice = self.parse_as(text) 

1045 if choice not in self.choices: 

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

1047 return choice 

1048 

1049 __call__ = parse 

1050 

1051 def __repr__(self): 

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

1053 

1054 

1055class FilePathParam: 

1056 """TODO 

1057 

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

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

1060 

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

1062 """ 

1063 

1064class FileValueParam: 

1065 """ 

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

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

1068 was on the argv. 

1069 """