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

495 statements  

1import sys 

2import shlex 

3import codecs 

4import os.path 

5from collections import OrderedDict 

6from typing import Optional 

7 

8from boltons.iterutils import split, unique 

9from boltons.dictutils import OrderedMultiDict as OMD 

10from boltons.funcutils import format_exp_repr, format_nonexp_repr 

11 

12from face.utils import (ERROR, 

13 get_type_desc, 

14 flag_to_identifier, 

15 normalize_flag_name, 

16 process_command_name, 

17 get_minimal_executable) 

18from face.errors import (FaceException, 

19 ArgumentParseError, 

20 ArgumentArityError, 

21 InvalidSubcommand, 

22 UnknownFlag, 

23 DuplicateFlag, 

24 InvalidFlagArgument, 

25 InvalidPositionalArgument, 

26 MissingRequiredFlags) 

27 

28 

29def _arg_to_subcmd(arg): 

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

31 

32 

33def _multi_error(flag, arg_val_list): 

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

35 if len(arg_val_list) > 1: 

36 raise DuplicateFlag.from_parse(flag, arg_val_list) 

37 return arg_val_list[0] 

38 

39 

40def _multi_extend(flag, arg_val_list): 

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

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

43 return ret 

44 

45 

46def _multi_override(flag, arg_val_list): 

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

48 return arg_val_list[-1] 

49 

50# TODO: _multi_ignore? 

51 

52_MULTI_SHORTCUTS = {'error': _multi_error, 

53 False: _multi_error, 

54 'extend': _multi_extend, 

55 True: _multi_extend, 

56 'override': _multi_override} 

57 

58 

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

60def _validate_char(char): 

61 orig_char = char 

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

63 char = char[1:] 

64 if len(char) > 1: 

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

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

67 if char not in _VALID_CHARS: 

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

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

70 return char 

71 

72 

73def _posargs_to_provides(posargspec, posargs): 

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

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

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

77 exactly one argument. 

78 

79 Cases as follows: 

80 

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

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

83 

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

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

86 help/error handler funcs when validation fails. 

87 ''' 

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

89 # <= max_count, etc. 

90 pas = posargspec 

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

92 return posargs 

93 if pas.max_count == 1: 

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

95 return posargs[0] if posargs else None 

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

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

98 

99 

100class CommandParseResult: 

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

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

103 argument corresponds 1:1 with an attribute. 

104 

105 Args: 

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

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

108 subcmds (tuple): Sequence of subcommand names. 

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

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

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

112 arguments (args following ``--``) 

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

114 result. Defaults to None. 

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

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

117 

118 Instances of this class can be injected by accepting the "args_" 

119 builtin in their Command handler function. 

120 

121 """ 

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

123 self.parser = parser 

124 self.argv = tuple(argv) 

125 

126 self.name = None # str 

127 self.subcmds = None # tuple 

128 self.flags = None # OrderedDict 

129 self.posargs = None # tuple 

130 self.post_posargs = None # tuple 

131 

132 def to_cmd_scope(self): 

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

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

135 

136 if not self.argv: 

137 cmd_ = self.parser.name 

138 else: 

139 cmd_ = self.argv[0] 

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

141 if basename == '__main__.py': 

142 pkg_name = os.path.basename(path) 

143 executable_path = get_minimal_executable() 

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

145 

146 ret = {'args_': self, 

147 'cmd_': cmd_, 

148 'subcmds_': self.subcmds, 

149 'flags_': self.flags, 

150 'posargs_': self.posargs, 

151 'post_posargs_': self.post_posargs, 

152 'subcommand_': _subparser, 

153 'command_': self.parser} 

154 if self.flags: 

155 ret.update(self.flags) 

156 

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

158 if prs.posargs.provides: 

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

160 ret[prs.posargs.provides] = posargs_provides 

161 if prs.post_posargs.provides: 

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

163 ret[prs.post_posargs.provides] = posargs_provides 

164 

165 return ret 

166 

167 def __repr__(self): 

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

169 

170 

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

172# char form? 

173class Flag: 

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

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

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

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

178 

179 Args: 

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

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

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

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

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

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

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

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

188 default flag will take one string argument. 

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

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

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

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

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

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

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

196 get the default behavior, which raises a DuplicateFlag 

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

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

199 :class:`CommandParseResult`. 

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

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

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

203 help generation. 

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

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

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

207 customizability. 

208 """ 

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

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

211 self.name = flag_to_identifier(name) 

212 self.doc = doc 

213 self.parse_as = parse_as 

214 self.missing = missing 

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

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

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

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

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

220 

221 if callable(multi): 

222 self.multi = multi 

223 elif multi in _MULTI_SHORTCUTS: 

224 self.multi = _MULTI_SHORTCUTS[multi] 

225 else: 

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

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

228 

229 self.set_display(display) 

230 

231 def set_display(self, display): 

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

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

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

235 customizability. 

236 """ 

237 if display is None: 

238 display = {} 

239 elif isinstance(display, bool): 

240 display = {'hidden': not display} 

241 elif isinstance(display, str): 

242 display = {'label': display} 

243 if isinstance(display, dict): 

244 display = FlagDisplay(self, **display) 

245 if not isinstance(display, FlagDisplay): 

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

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

248 % display) 

249 self.display = display 

250 

251 def __repr__(self): 

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

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

254 

255 

256class FlagDisplay: 

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

258 settings, as used by HelpFormatter instances attached to Parser 

259 and Command objects. Pass an instance of this to 

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

261 

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

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

264 Flag. They are generally automatically created by a Flag 

265 constructor, based on the "display" argument. 

266 

267 Args: 

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

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

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

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

272 HelpFormatter. 

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

274 doc. Defaults to a parenthetical describing whether the flag 

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

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

277 the doc + post_doc default. 

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

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

280 error labels. 

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

282 error messages. Defaults to False. 

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

284 grouped in help messages, improving readability. Integers are 

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

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

287 string to override the sort order. 

288 

289 """ 

290 # value_name -> arg_name? 

291 def __init__(self, flag, *, 

292 label: Optional[str] = None, 

293 post_doc: Optional[str] = None, 

294 full_doc: Optional[str] = None, 

295 value_name: Optional[str] = None, 

296 group: int = 0, 

297 hidden: bool = False, 

298 sort_key: int = 0): 

299 self.flag = flag 

300 

301 self.doc = flag.doc 

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

303 _prep, desc = get_type_desc(flag.parse_as) 

304 self.doc = 'Parsed with ' + desc 

305 if _prep == 'as': 

306 self.doc = desc 

307 

308 self.post_doc = post_doc 

309 self.full_doc = full_doc 

310 

311 self.value_name = '' 

312 if callable(flag.parse_as): 

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

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

315 

316 self.group = group 

317 self._hide = hidden 

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

319 self.sort_key = sort_key 

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

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

322 # in the order they are created 

323 return 

324 

325 @property 

326 def hidden(self): 

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

328 

329 def __repr__(self): 

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

331 

332 

333class PosArgDisplay: 

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

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

336 turn passed to a Command/Parser. 

337 

338 Args: 

339 spec (PosArgSpec): The associated PosArgSpec. 

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

341 argument. Automatically pluralized in the label according to 

342 PosArgSpec values. Defaults to 'arg'. 

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

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

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

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

347 often describes default behavior. 

348 

349 """ 

350 def __init__(self, *, 

351 name: Optional[str] = None, 

352 doc: str = '', 

353 post_doc: Optional[str] = None, 

354 hidden: bool = False, 

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

356 self.name = name or 'arg' 

357 self.doc = doc 

358 self.post_doc = post_doc 

359 self._hide = hidden 

360 self.label = label 

361 

362 @property 

363 def hidden(self): 

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

365 

366 def __repr__(self): 

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

368 

369 

370class PosArgSpec: 

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

372 configure the number and type of positional arguments. 

373 

374 Args: 

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

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

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

378 min_count (int): A minimimum number of positional 

379 arguments. Defaults to 0. 

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

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

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

383 False to hide it completely. Also accepts a PosArgDisplay 

384 instance, or a dict of the respective arguments. 

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

386 handler function. 

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

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

389 when an exact number of arguments should be specified. 

390 

391 PosArgSpec instances are stateless and safe to be used multiple 

392 times around the application. 

393 

394 """ 

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

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

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

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

399 

400 self.parse_as = parse_as 

401 

402 # count convenience alias 

403 min_count = count if min_count is None else min_count 

404 max_count = count if max_count is None else max_count 

405 

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

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

408 

409 if self.min_count < 0: 

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

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

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

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

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

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

416 

417 provides = name if provides is None else provides 

418 self.provides = provides 

419 

420 if display is None: 

421 display = {} 

422 elif isinstance(display, bool): 

423 display = {'hidden': not display} 

424 elif isinstance(display, str): 

425 display = {'name': display} 

426 if isinstance(display, dict): 

427 display.setdefault('name', name) 

428 display = PosArgDisplay(**display) 

429 if not isinstance(display, PosArgDisplay): 

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

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

432 % display) 

433 

434 self.display = display 

435 

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

437 

438 def __repr__(self): 

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

440 

441 @property 

442 def accepts_args(self): 

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

444 more arguments. 

445 """ 

446 return self.parse_as is not ERROR 

447 

448 def parse(self, posargs): 

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

450 

451 Args: 

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

453 instance from sys.argv. 

454 

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

456 arguments. 

457 

458 Raises InvalidPositionalArgument if the argument doesn't match 

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

460 

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

462 """ 

463 len_posargs = len(posargs) 

464 if posargs and not self.accepts_args: 

465 # TODO: check for likely subcommands 

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

467 min_count, max_count = self.min_count, self.max_count 

468 if min_count == max_count: 

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

470 arg_range_text = f'{min_count} argument' 

471 if min_count > 1: 

472 arg_range_text += 's' 

473 else: 

474 if min_count == 0: 

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

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

477 elif max_count is None: 

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

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

480 else: 

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

482 

483 if len_posargs < min_count: 

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

485 % (arg_range_text, len_posargs)) 

486 if max_count is not None and len_posargs > max_count: 

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

488 % (arg_range_text, len_posargs)) 

489 ret = [] 

490 for pa in posargs: 

491 try: 

492 val = self.parse_as(pa) 

493 except Exception as exc: 

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

495 else: 

496 ret.append(val) 

497 return ret 

498 

499 

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

501 

502 

503def _ensure_posargspec(posargs, posargs_name): 

504 if not posargs: 

505 # take no posargs 

506 posargs = PosArgSpec(parse_as=ERROR) 

507 elif posargs is True: 

508 # take any number of posargs 

509 posargs = PosArgSpec() 

510 elif isinstance(posargs, int): 

511 # take an exact number of posargs 

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

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

514 elif isinstance(posargs, str): 

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

516 elif isinstance(posargs, dict): 

517 posargs = PosArgSpec(**posargs) 

518 elif callable(posargs): 

519 # take any number of posargs of a given format 

520 posargs = PosArgSpec(parse_as=posargs) 

521 

522 if not isinstance(posargs, PosArgSpec): 

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

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

525 % (posargs_name, posargs)) 

526 

527 return posargs 

528 

529 

530class Parser: 

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

532 configurable validation logic on top of the conventional grammar 

533 for CLI argument parsing. 

534 

535 Args: 

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

537 when the command is embedded as a subcommand of another 

538 command. 

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

540 to generate help and usage information. 

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

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

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

544 the Parser to accept positional arguments. Pass a callable 

545 to parse the positional arguments using that 

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

547 customizability. 

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

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

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

551 style of positional argument. 

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

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

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

555 Flagfiles below. 

556 

557 Once initialized, parsing is performed by calling 

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

559 """ 

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

561 post_posargs=None, flagfile=True): 

562 self.name = process_command_name(name) 

563 self.doc = doc 

564 flags = list(flags or []) 

565 

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

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

568 

569 if flagfile is True: 

570 self.flagfile_flag = FLAGFILE_ENABLED 

571 elif isinstance(flagfile, Flag): 

572 self.flagfile_flag = flagfile 

573 elif not flagfile: 

574 self.flagfile_flag = None 

575 else: 

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

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

578 

579 self.subprs_map = OrderedDict() 

580 self._path_flag_map = OrderedDict() 

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

582 

583 for flag in flags: 

584 self.add(flag) 

585 if self.flagfile_flag: 

586 self.add(self.flagfile_flag) 

587 return 

588 

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

590 flag_map = self._path_flag_map[path] 

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

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

593 

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

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

596 

597 return unique(flag_map.values()) 

598 

599 def __repr__(self): 

600 cn = self.__class__.__name__ 

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

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

603 

604 def _add_subparser(self, subprs): 

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

606 subcommand flag conflicts, then finally add subcommand. 

607 

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

609 that parser or command with a different name. 

610 """ 

611 if self.posargs.accepts_args: 

612 raise ValueError('commands accepting positional arguments' 

613 ' cannot take subcommands') 

614 

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

616 subprs_name = process_command_name(subprs.name) 

617 

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

619 for prs_path in self.subprs_map: 

620 if prs_path[0] == subprs_name: 

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

622 parent_flag_map = self._path_flag_map[()] 

623 

624 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True 

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

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

627 # TODO 

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

629 

630 # with checks complete, add parser and all subparsers 

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

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

633 new_path = (subprs_name,) + path 

634 self.subprs_map[new_path] = cur_subprs 

635 

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

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

638 new_flags = parent_flag_map.copy() 

639 new_flags.update(flags) 

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

641 

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

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

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

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

646 # for defaults? 

647 

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

649 """Add a flag or subparser. 

650 

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

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

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

654 

655 May raise ValueError if arguments are not recognized as 

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

657 raised on duplicate definitions and other conflicts. 

658 """ 

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

660 subprs = a[0] 

661 self._add_subparser(subprs) 

662 return 

663 

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

665 flag = a[0] 

666 else: 

667 try: 

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

669 except TypeError as te: 

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

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

672 return self._add_flag(flag) 

673 

674 def _add_flag(self, flag): 

675 # first check there are no conflicts... 

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

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

678 if conflict_flag is None: 

679 continue 

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

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

682 % (conflict_flag, flag.name)) 

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

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

685 % (conflict_flag, flag)) 

686 

687 # ... then we add the flags 

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

689 flag_map[flag.name] = flag 

690 if flag.char: 

691 flag_map[flag.char] = flag 

692 return 

693 

694 def parse(self, argv): 

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

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

697 subparsers, and other options configured. 

698 

699 Args: 

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

701 use ``sys.argv``. 

702 

703 This method may raise ArgumentParseError (or one of its 

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

705 

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

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

708 implementing codebases to perform that sort of 

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

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

711 that the Python CLI application has some sort of 

712 programmatic interface that doesn't require 

713 subprocessing. See here for an example. 

714 

715 """ 

716 if argv is None: 

717 argv = sys.argv 

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

719 if not argv: 

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

721 ape.prs_res = cpr 

722 raise ape 

723 for arg in argv: 

724 if not isinstance(arg, str): 

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

726 ''' 

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

728 if len(subprs_path) == 1: 

729 # _add_subparser takes care of recurring so we only 

730 # need direct subparser descendants 

731 self._add_subparser(subprs, overwrite=True) 

732 ''' 

733 flag_map = None 

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

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

736 cpr.name = cmd_name 

737 

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

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

740 

741 try: 

742 # then figure out the subcommand path 

743 subcmds, args = self._parse_subcmds(args) 

744 cpr.subcmds = tuple(subcmds) 

745 

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

747 

748 # then look up the subcommand's supported flags 

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

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

751 # available arguments. 

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

753 

754 # parse supported flags and validate their arguments 

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

756 cpr.flags = OrderedDict(flag_map) 

757 cpr.posargs = tuple(posargs) 

758 

759 # take care of dupes and check required flags 

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

761 cpr.flags = OrderedDict(resolved_flag_map) 

762 

763 # separate out any trailing arguments from normal positional arguments 

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

765 parsed_post_posargs = None 

766 if '--' in posargs: 

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

768 cpr.posargs, cpr.post_posargs = posargs, post_posargs 

769 

770 parsed_post_posargs = prs.post_posargs.parse(post_posargs) 

771 cpr.post_posargs = tuple(parsed_post_posargs) 

772 

773 parsed_posargs = prs.posargs.parse(posargs) 

774 cpr.posargs = tuple(parsed_posargs) 

775 except ArgumentParseError as ape: 

776 ape.prs_res = cpr 

777 raise 

778 

779 return cpr 

780 

781 def _parse_subcmds(self, args): 

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

783 

784 Returns a tuple of (list_of_subcmds, remaining_args). 

785 

786 Raises on unknown subcommands.""" 

787 ret = [] 

788 

789 for arg in args: 

790 if arg.startswith('-'): 

791 break # subcmd parsing complete 

792 

793 arg = _arg_to_subcmd(arg) 

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

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

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

797 # we actually have posargs from here 

798 break 

799 raise InvalidSubcommand.from_parse(prs, arg) 

800 ret.append(arg) 

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

802 

803 def _parse_single_flag(self, cmd_flag_map, args): 

804 advance = 1 

805 arg = args[0] 

806 arg_text = None 

807 try: 

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

809 except ValueError: 

810 pass 

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

812 if flag is None: 

813 raise UnknownFlag.from_parse(cmd_flag_map, arg) 

814 parse_as = flag.parse_as 

815 if not callable(parse_as): 

816 if arg_text: 

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

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

819 return flag, parse_as, args[1:] 

820 

821 try: 

822 if arg_text is None: 

823 arg_text = args[1] 

824 advance = 2 

825 except IndexError: 

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

827 try: 

828 arg_val = parse_as(arg_text) 

829 except Exception as e: 

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

831 

832 return flag, arg_val, args[advance:] 

833 

834 def _parse_flags(self, cmd_flag_map, args): 

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

836 the second item returned from _parse_subcmds) 

837 

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

839 

840 Raises on unknown subcommands. 

841 """ 

842 flag_value_map = OMD() 

843 ff_path_res_map = OrderedDict() 

844 ff_path_seen = set() 

845 

846 orig_args = args 

847 while args: 

848 arg = args[0] 

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

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

851 break 

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

853 flag_value_map.add(flag.name, value) 

854 

855 if flag is self.flagfile_flag: 

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

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

858 if path in ff_path_seen: 

859 continue 

860 flag_value_map.update_extend(ff_flag_value_map) 

861 ff_path_seen.add(path) 

862 

863 return flag_value_map, ff_path_res_map, args 

864 

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

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

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

868 # enable StringIO and custom flagfile opening 

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

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

871 ff_text = path_or_file.read() 

872 else: 

873 path = os.path.abspath(path_or_file) 

874 try: 

875 with codecs.open(path_or_file, 'r', 'utf-8') as f: 

876 ff_text = f.read() 

877 except (UnicodeError, OSError) as ee: 

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

879 if path in res_map: 

880 # we've already seen this file 

881 return res_map 

882 ret[path] = cur_file_res = OMD() 

883 lines = ff_text.splitlines() 

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

885 try: 

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

887 if not args: 

888 continue # comment or empty line 

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

890 

891 if leftover_args: 

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

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

894 

895 cur_file_res.add(flag.name, value) 

896 if flag is self.flagfile_flag: 

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

898 

899 except FaceException as fe: 

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

901 raise 

902 

903 return ret 

904 

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

906 ret = OrderedDict() 

907 cfm, pfm = cmd_flag_map, parsed_flag_map 

908 flagfile_map = flagfile_map or {} 

909 

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

911 missing_flags = [] 

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

913 if flag.name in pfm: 

914 continue 

915 if flag.missing is ERROR: 

916 missing_flags.append(flag.name) 

917 else: 

918 pfm[flag.name] = flag.missing 

919 if missing_flags: 

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

921 

922 # ... resolve dupes 

923 for flag_name in pfm: 

924 flag = cfm[flag_name] 

925 arg_val_list = pfm.getlist(flag_name) 

926 try: 

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

928 except FaceException as fe: 

929 ff_paths = [] 

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

931 if flag_name in ff_value_map: 

932 ff_paths.append(ff_path) 

933 if ff_paths: 

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

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

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

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

938 raise 

939 return ret 

940 

941 

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

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

944 *sep*. Supports quoting. 

945 

946 """ 

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

948 # handled at the layer above. 

949 from csv import reader, Dialect, QUOTE_MINIMAL 

950 

951 class _face_dialect(Dialect): 

952 delimiter = sep 

953 escapechar = '\\' 

954 quotechar = '"' 

955 doublequote = True 

956 skipinitialspace = False 

957 lineterminator = '\n' 

958 quoting = QUOTE_MINIMAL 

959 

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

961 return parsed[0] 

962 

963 

964class ListParam: 

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

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

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

968 

969 --flag a1,b2,c3 

970 

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

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

973 quoting when values themselves contain the separator:: 

974 

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

976 

977 Args: 

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

979 parsed value. 

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

981 value separator. Defaults to ``,``. 

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

983 whitespace stripped before being passed to 

984 *parse_one_as*. Defaults to False. 

985 

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

987 accepting multiple arguments is to use the 

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

989 approach tends to be more verbose and can be confusing 

990 because arguments can get spread across the command 

991 line. 

992 

993 """ 

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

995 # TODO: min/max limits? 

996 self.parse_one_as = parse_one_as 

997 self.sep = sep 

998 self.strip = strip 

999 

1000 def parse(self, list_text): 

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

1002 split_vals = parse_sv_line(list_text, self.sep) 

1003 if self.strip: 

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

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

1006 

1007 __call__ = parse 

1008 

1009 def __repr__(self): 

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

1011 

1012 

1013class ChoicesParam: 

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

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

1016 an explicit one can be set *parse_as*. 

1017 """ 

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

1019 if not choices: 

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

1021 try: 

1022 self.choices = sorted(choices) 

1023 except Exception: 

1024 # in case choices aren't sortable 

1025 self.choices = list(choices) 

1026 if parse_as is None: 

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

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

1029 self.parse_as = parse_as 

1030 

1031 def parse(self, text): 

1032 choice = self.parse_as(text) 

1033 if choice not in self.choices: 

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

1035 return choice 

1036 

1037 __call__ = parse 

1038 

1039 def __repr__(self): 

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

1041 

1042 

1043class FilePathParam: 

1044 """TODO 

1045 

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

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

1048 

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

1050 """ 

1051 

1052class FileValueParam: 

1053 """ 

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

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

1056 was on the argv. 

1057 """