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

494 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:23 +0000

1 

2import sys 

3import shlex 

4import codecs 

5import os.path 

6from collections import OrderedDict 

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 

28try: 

29 unicode 

30except NameError: 

31 unicode = str 

32 

33 

34def _arg_to_subcmd(arg): 

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

36 

37 

38def _multi_error(flag, arg_val_list): 

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

40 if len(arg_val_list) > 1: 

41 raise DuplicateFlag.from_parse(flag, arg_val_list) 

42 return arg_val_list[0] 

43 

44 

45def _multi_extend(flag, arg_val_list): 

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

47 return arg_val_list 

48 

49 

50def _multi_override(flag, arg_val_list): 

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

52 return arg_val_list[-1] 

53 

54# TODO: _multi_ignore? 

55 

56_MULTI_SHORTCUTS = {'error': _multi_error, 

57 False: _multi_error, 

58 'extend': _multi_extend, 

59 True: _multi_extend, 

60 'override': _multi_override} 

61 

62 

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

64def _validate_char(char): 

65 orig_char = char 

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

67 char = char[1:] 

68 if len(char) > 1: 

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

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

71 if char not in _VALID_CHARS: 

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

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

74 return char 

75 

76 

77def _posargs_to_provides(posargspec, posargs): 

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

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

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

81 exactly one argument. 

82 

83 Cases as follows: 

84 

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

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

87 

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

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

90 help/error handler funcs when validation fails. 

91 ''' 

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

93 # <= max_count, etc. 

94 pas = posargspec 

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

96 return posargs 

97 if pas.max_count == 1: 

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

99 return posargs[0] if posargs else None 

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

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

102 

103 

104class CommandParseResult(object): 

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

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

107 argument corresponds 1:1 with an attribute. 

108 

109 Args: 

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

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

112 subcmds (tuple): Sequence of subcommand names. 

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

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

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

116 arguments (args following ``--``) 

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

118 result. Defaults to None. 

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

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

121 

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

123 builtin in their Command handler function. 

124 

125 """ 

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

127 self.parser = parser 

128 self.argv = tuple(argv) 

129 

130 self.name = None # str 

131 self.subcmds = None # tuple 

132 self.flags = None # OrderedDict 

133 self.posargs = None # tuple 

134 self.post_posargs = None # tuple 

135 

136 def to_cmd_scope(self): 

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

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

139 

140 if not self.argv: 

141 cmd_ = self.parser.name 

142 else: 

143 cmd_ = self.argv[0] 

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

145 if basename == '__main__.py': 

146 pkg_name = os.path.basename(path) 

147 executable_path = get_minimal_executable() 

148 cmd_ = '%s -m %s' % (executable_path, pkg_name) 

149 

150 ret = {'args_': self, 

151 'cmd_': cmd_, 

152 'subcmds_': self.subcmds, 

153 'flags_': self.flags, 

154 'posargs_': self.posargs, 

155 'post_posargs_': self.post_posargs, 

156 'subcommand_': _subparser, 

157 'command_': self.parser} 

158 if self.flags: 

159 ret.update(self.flags) 

160 

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

162 if prs.posargs.provides: 

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

164 ret[prs.posargs.provides] = posargs_provides 

165 if prs.post_posargs.provides: 

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

167 ret[prs.post_posargs.provides] = posargs_provides 

168 

169 return ret 

170 

171 def __repr__(self): 

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

173 

174 

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

176# char form? 

177class Flag(object): 

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

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

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

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

182 

183 Args: 

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

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

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

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

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

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

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

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

192 default flag will take one string argument. 

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

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

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

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

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

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

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

200 get the default behavior, which raises a DuplicateFlag 

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

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

203 :class:`CommandParseResult`. 

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

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

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

207 help generation. 

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

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

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

211 customizability. 

212 """ 

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

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

215 self.name = flag_to_identifier(name) 

216 self.doc = doc 

217 self.parse_as = parse_as 

218 self.missing = missing 

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

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

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

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

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

224 

225 if callable(multi): 

226 self.multi = multi 

227 elif multi in _MULTI_SHORTCUTS: 

228 self.multi = _MULTI_SHORTCUTS[multi] 

229 else: 

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

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

232 

233 self.set_display(display) 

234 

235 def set_display(self, display): 

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

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

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

239 customizability. 

240 """ 

241 if display is None: 

242 display = {} 

243 elif isinstance(display, bool): 

244 display = {'hidden': not display} 

245 elif isinstance(display, str): 

246 display = {'label': display} 

247 if isinstance(display, dict): 

248 display = FlagDisplay(self, **display) 

249 if not isinstance(display, FlagDisplay): 

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

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

252 % display) 

253 self.display = display 

254 

255 def __repr__(self): 

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

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

258 

259 

260class FlagDisplay(object): 

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

262 settings, as used by HelpFormatter instances attached to Parser 

263 and Command objects. Pass an instance of this to 

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

265 

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

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

268 Flag. They are generally automatically created by a Flag 

269 constructor, based on the "display" argument. 

270 

271 Args: 

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

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

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

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

276 HelpFormatter. 

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

278 doc. Defaults to a parenthetical describing whether the flag 

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

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

281 the doc + post_doc default. 

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

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

284 error labels. 

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

286 error messages. Defaults to False. 

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

288 grouped in help messages, improving readability. Integers are 

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

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

291 string to override the sort order. 

292 

293 """ 

294 # value_name -> arg_name? 

295 def __init__(self, flag, **kw): 

296 self.flag = flag 

297 

298 self.doc = flag.doc 

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

300 _prep, desc = get_type_desc(flag.parse_as) 

301 self.doc = 'Parsed with ' + desc 

302 if _prep == 'as': 

303 self.doc = desc 

304 

305 self.post_doc = kw.pop('post_doc', None) 

306 self.full_doc = kw.pop('full_doc', None) 

307 

308 self.value_name = '' 

309 if callable(flag.parse_as): 

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

311 self.value_name = kw.pop('value_name', None) or self.flag.name.upper() 

312 

313 self.group = kw.pop('group', 0) # int or str 

314 self._hide = kw.pop('hidden', False) # bool 

315 self.label = kw.pop('label', None) # see hidden property below for more info 

316 self.sort_key = kw.pop('sort_key', 0) # int or str 

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

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

319 # in the order they are created 

320 

321 if kw: 

322 raise TypeError('unexpected keyword arguments: %r' % kw.keys()) 

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(object): 

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, **kw): 

351 self.name = kw.pop('name', 'arg') 

352 self.doc = kw.pop('doc', '') 

353 self.post_doc = kw.pop('post_doc', None) 

354 self._hide = kw.pop('hidden', False) # bool 

355 self.label = kw.pop('label', None) 

356 

357 if kw: 

358 raise TypeError('unexpected keyword arguments: %r' % kw.keys()) 

359 return 

360 

361 @property 

362 def hidden(self): 

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

364 

365 def __repr__(self): 

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

367 

368 

369class PosArgSpec(object): 

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

371 configure the number and type of positional arguments. 

372 

373 Args: 

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

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

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

377 min_count (int): A minimimum number of positional 

378 arguments. Defaults to 0. 

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

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

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

382 False to hide it completely. Also accepts a PosArgDisplay 

383 instance, or a dict of the respective arguments. 

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

385 handler function. 

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

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

388 when an exact number of arguments should be specified. 

389 

390 PosArgSpec instances are stateless and safe to be used multiple 

391 times around the application. 

392 

393 """ 

394 def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None, **kwargs): 

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

396 raise TypeError('expected callable or ERROR for parse_as, not %r' % parse_as) 

397 name = kwargs.pop('name', None) 

398 count = kwargs.pop('count', None) 

399 if kwargs: 

400 raise TypeError('unexpected keyword arguments: %r' % list(kwargs.keys())) 

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('expected min_count >= 0, not: %r' % self.min_count) 

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

413 raise ValueError('expected max_count > 0, not: %r' % self.max_count) 

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('unexpected positional arguments: %r' % posargs) 

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 = '%s argument' % min_count 

472 if min_count > 1: 

473 arg_range_text += 's' 

474 else: 

475 if min_count == 0: 

476 arg_range_text = 'up to %s argument' % max_count 

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

478 elif max_count is None: 

479 arg_range_text = 'at least %s argument' % min_count 

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

481 else: 

482 arg_range_text = '%s - %s arguments' % (min_count, max_count) 

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(object): 

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): 

563 self.name = process_command_name(name) 

564 self.doc = doc 

565 flags = list(flags or []) 

566 

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

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

569 

570 if flagfile is True: 

571 self.flagfile_flag = FLAGFILE_ENABLED 

572 elif isinstance(flagfile, Flag): 

573 self.flagfile_flag = flagfile 

574 elif not flagfile: 

575 self.flagfile_flag = None 

576 else: 

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

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

579 

580 self.subprs_map = OrderedDict() 

581 self._path_flag_map = OrderedDict() 

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

583 

584 for flag in flags: 

585 self.add(flag) 

586 if self.flagfile_flag: 

587 self.add(self.flagfile_flag) 

588 return 

589 

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

591 flag_map = self._path_flag_map[path] 

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

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

594 

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

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

597 

598 return unique(flag_map.values()) 

599 

600 def __repr__(self): 

601 cn = self.__class__.__name__ 

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

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

604 

605 def _add_subparser(self, subprs): 

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

607 subcommand flag conflicts, then finally add subcommand. 

608 

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

610 that parser or command with a different name. 

611 """ 

612 if self.posargs.accepts_args: 

613 raise ValueError('commands accepting positional arguments' 

614 ' cannot take subcommands') 

615 

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

617 subprs_name = process_command_name(subprs.name) 

618 

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

620 for prs_path in self.subprs_map: 

621 if prs_path[0] == subprs_name: 

622 raise ValueError('conflicting subcommand name: %r' % subprs_name) 

623 parent_flag_map = self._path_flag_map[()] 

624 

625 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True 

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

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

628 # TODO 

629 raise ValueError('subcommand flags conflict with parent command: %r' % flags) 

630 

631 # with checks complete, add parser and all subparsers 

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

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

634 new_path = (subprs_name,) + path 

635 self.subprs_map[new_path] = cur_subprs 

636 

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

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

639 new_flags = parent_flag_map.copy() 

640 new_flags.update(flags) 

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

642 

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

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

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

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

647 # for defaults? 

648 

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

650 """Add a flag or subparser. 

651 

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

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

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

655 

656 May raise ValueError if arguments are not recognized as 

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

658 raised on duplicate definitions and other conflicts. 

659 """ 

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

661 subprs = a[0] 

662 self._add_subparser(subprs) 

663 return 

664 

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

666 flag = a[0] 

667 else: 

668 try: 

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

670 except TypeError as te: 

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

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

673 

674 # first check there are no conflicts... 

675 flag.name = flag.name 

676 

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

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

679 if conflict_flag is None: 

680 continue 

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

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

683 % (conflict_flag, flag.name)) 

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

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

686 % (conflict_flag, flag)) 

687 

688 # ... then we add the flags 

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

690 flag_map[flag.name] = flag 

691 if flag.char: 

692 flag_map[flag.char] = flag 

693 return 

694 

695 def parse(self, argv): 

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

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

698 subparsers, and other options configured. 

699 

700 Args: 

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

702 use ``sys.argv``. 

703 

704 This method may raise ArgumentParseError (or one of its 

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

706 

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

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

709 implementing codebases to perform that sort of 

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

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

712 that the Python CLI application has some sort of 

713 programmatic interface that doesn't require 

714 subprocessing. See here for an example. 

715 

716 """ 

717 if argv is None: 

718 argv = sys.argv 

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

720 if not argv: 

721 ape = ArgumentParseError('expected non-empty sequence of arguments, not: %r' % (argv,)) 

722 ape.prs_res = cpr 

723 raise ape 

724 for arg in argv: 

725 if not isinstance(arg, (str, unicode)): 

726 raise TypeError('parse expected all args as strings, not: %r (%s)' % (arg, type(arg).__name__)) 

727 ''' 

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

729 if len(subprs_path) == 1: 

730 # _add_subparser takes care of recurring so we only 

731 # need direct subparser descendants 

732 self._add_subparser(subprs, overwrite=True) 

733 ''' 

734 flag_map = None 

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

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

737 cpr.name = cmd_name 

738 

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

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

741 

742 try: 

743 # then figure out the subcommand path 

744 subcmds, args = self._parse_subcmds(args) 

745 cpr.subcmds = tuple(subcmds) 

746 

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

748 

749 # then look up the subcommand's supported flags 

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

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

752 # available arguments. 

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

754 

755 # parse supported flags and validate their arguments 

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

757 cpr.flags = OrderedDict(flag_map) 

758 cpr.posargs = tuple(posargs) 

759 

760 # take care of dupes and check required flags 

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

762 cpr.flags = OrderedDict(resolved_flag_map) 

763 

764 # separate out any trailing arguments from normal positional arguments 

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

766 parsed_post_posargs = None 

767 if '--' in posargs: 

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

769 cpr.posargs, cpr.post_posargs = posargs, post_posargs 

770 

771 parsed_post_posargs = prs.post_posargs.parse(post_posargs) 

772 cpr.post_posargs = tuple(parsed_post_posargs) 

773 

774 parsed_posargs = prs.posargs.parse(posargs) 

775 cpr.posargs = tuple(parsed_posargs) 

776 except ArgumentParseError as ape: 

777 ape.prs_res = cpr 

778 raise 

779 

780 return cpr 

781 

782 def _parse_subcmds(self, args): 

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

784 

785 Returns a tuple of (list_of_subcmds, remaining_args). 

786 

787 Raises on unknown subcommands.""" 

788 ret = [] 

789 

790 for arg in args: 

791 if arg.startswith('-'): 

792 break # subcmd parsing complete 

793 

794 arg = _arg_to_subcmd(arg) 

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

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

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

798 # we actually have posargs from here 

799 break 

800 raise InvalidSubcommand.from_parse(prs, arg) 

801 ret.append(arg) 

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

803 

804 def _parse_single_flag(self, cmd_flag_map, args): 

805 arg = args[0] 

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

807 if flag is None: 

808 raise UnknownFlag.from_parse(cmd_flag_map, arg) 

809 parse_as = flag.parse_as 

810 if not callable(parse_as): 

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

812 return flag, parse_as, args[1:] 

813 

814 try: 

815 arg_text = args[1] 

816 except IndexError: 

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

818 try: 

819 arg_val = parse_as(arg_text) 

820 except Exception as e: 

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

822 

823 return flag, arg_val, args[2:] 

824 

825 def _parse_flags(self, cmd_flag_map, args): 

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

827 the second item returned from _parse_subcmds) 

828 

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

830 

831 Raises on unknown subcommands. 

832 """ 

833 flag_value_map = OMD() 

834 ff_path_res_map = OrderedDict() 

835 ff_path_seen = set() 

836 

837 orig_args = args 

838 while args: 

839 arg = args[0] 

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

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

842 break 

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

844 flag_value_map.add(flag.name, value) 

845 

846 if flag is self.flagfile_flag: 

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

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

849 if path in ff_path_seen: 

850 continue 

851 flag_value_map.update_extend(ff_flag_value_map) 

852 ff_path_seen.add(path) 

853 

854 return flag_value_map, ff_path_res_map, args 

855 

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

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

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

859 # enable StringIO and custom flagfile opening 

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

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

862 ff_text = path_or_file.read() 

863 else: 

864 path = os.path.abspath(path_or_file) 

865 try: 

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

867 ff_text = f.read() 

868 except (UnicodeError, EnvironmentError) as ee: 

869 raise ArgumentParseError('failed to load flagfile "%s", got: %r' % (path, ee)) 

870 if path in res_map: 

871 # we've already seen this file 

872 return res_map 

873 ret[path] = cur_file_res = OMD() 

874 lines = ff_text.splitlines() 

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

876 try: 

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

878 if not args: 

879 continue # comment or empty line 

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

881 

882 if leftover_args: 

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

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

885 

886 cur_file_res.add(flag.name, value) 

887 if flag is self.flagfile_flag: 

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

889 

890 except FaceException as fe: 

891 fe.args = (fe.args[0] + ' (on line %s of flagfile "%s")' % (lineno, path),) 

892 raise 

893 

894 return ret 

895 

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

897 ret = OrderedDict() 

898 cfm, pfm = cmd_flag_map, parsed_flag_map 

899 flagfile_map = flagfile_map or {} 

900 

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

902 missing_flags = [] 

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

904 if flag_name in pfm: 

905 continue 

906 if flag.missing is ERROR: 

907 missing_flags.append(flag.name) 

908 else: 

909 pfm[flag_name] = flag.missing 

910 if missing_flags: 

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

912 

913 # ... resolve dupes 

914 for flag_name in pfm: 

915 flag = cfm[flag_name] 

916 arg_val_list = pfm.getlist(flag_name) 

917 try: 

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

919 except FaceException as fe: 

920 ff_paths = [] 

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

922 if flag_name in ff_value_map: 

923 ff_paths.append(ff_path) 

924 if ff_paths: 

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

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

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

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

929 raise 

930 return ret 

931 

932 

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

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

935 *sep*. Supports quoting. 

936 

937 """ 

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

939 # handled at the layer above. 

940 from csv import reader, Dialect, QUOTE_MINIMAL 

941 

942 class _face_dialect(Dialect): 

943 delimiter = sep 

944 escapechar = '\\' 

945 quotechar = '"' 

946 doublequote = True 

947 skipinitialspace = False 

948 lineterminator = '\n' 

949 quoting = QUOTE_MINIMAL 

950 

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

952 return parsed[0] 

953 

954 

955class ListParam(object): 

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

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

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

959 

960 --flag a1,b2,c3 

961 

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

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

964 quoting when values themselves contain the separator:: 

965 

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

967 

968 Args: 

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

970 parsed value. 

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

972 value separator. Defaults to ``,``. 

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

974 whitespace stripped before being passed to 

975 *parse_one_as*. Defaults to False. 

976 

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

978 accepting multiple arguments is to use the 

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

980 approach tends to be more verbose and can be confusing 

981 because arguments can get spread across the command 

982 line. 

983 

984 """ 

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

986 # TODO: min/max limits? 

987 self.parse_one_as = parse_one_as 

988 self.sep = sep 

989 self.strip = strip 

990 

991 def parse(self, list_text): 

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

993 split_vals = parse_sv_line(list_text, self.sep) 

994 if self.strip: 

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

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

997 

998 __call__ = parse 

999 

1000 def __repr__(self): 

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

1002 

1003 

1004class ChoicesParam(object): 

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

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

1007 an explicit one can be set *parse_as*. 

1008 """ 

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

1010 if not choices: 

1011 raise ValueError('expected at least one choice, not: %r' % choices) 

1012 try: 

1013 self.choices = sorted(choices) 

1014 except Exception: 

1015 # in case choices aren't sortable 

1016 self.choices = list(choices) 

1017 if parse_as is None: 

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

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

1020 self.parse_as = parse_as 

1021 

1022 def parse(self, text): 

1023 choice = self.parse_as(text) 

1024 if choice not in self.choices: 

1025 raise ArgumentParseError('expected one of %r, not: %r' % (self.choices, text)) 

1026 return choice 

1027 

1028 __call__ = parse 

1029 

1030 def __repr__(self): 

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

1032 

1033 

1034class FilePathParam(object): 

1035 """TODO 

1036 

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

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

1039 

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

1041 """ 

1042 

1043class FileValueParam(object): 

1044 """ 

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

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

1047 was on the argv. 

1048 """