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
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
1import sys
2import shlex
3import os.path
4from collections import OrderedDict
5from typing import Optional
7from boltons.iterutils import split, unique
8from boltons.dictutils import OrderedMultiDict as OMD
9from boltons.funcutils import format_exp_repr, format_nonexp_repr
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)
28def _arg_to_subcmd(arg):
29 return arg.lower().replace('-', '_')
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]
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
45def _multi_override(flag, arg_val_list):
46 "Return only the last argument specified for a flag"
47 return arg_val_list[-1]
49# TODO: _multi_ignore?
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}
61_MULTI_SHORTCUT_NAMES = ('error', 'extend', 'override')
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
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.
84 Cases as follows:
86 1. min_count > 1 or max_count > 1, pass through posargs as a list
87 2. max_count == 1 -> single argument or None
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)
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.
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 ``()``.
123 Instances of this class can be injected by accepting the ``args_``
124 builtin in their Command handler function.
126 """
127 def __init__(self, parser, argv=()):
128 self.parser = parser
129 self.argv = tuple(argv)
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
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
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_)
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)
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
172 return ret
174 def __repr__(self):
175 return format_nonexp_repr(self, ['name', 'argv', 'parser'])
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
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
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))
240 self.set_display(display)
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
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))
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.
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.
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.
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
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
319 self.post_doc = post_doc
320 self.full_doc = full_doc
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()
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
336 @property
337 def hidden(self):
338 return self._hide or self.label == ''
340 def __repr__(self):
341 return format_nonexp_repr(self, ['label', 'doc'], ['group', 'hidden'], opt_key=bool)
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.
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.
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
373 @property
374 def hidden(self):
375 return self._hide or self.label == ''
377 def __repr__(self):
378 return format_nonexp_repr(self, ['name', 'label'])
381class PosArgSpec:
382 """Passed to Command/Parser as posargs and post_posargs parameters to
383 configure the number and type of positional arguments.
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.
402 PosArgSpec instances are stateless and safe to be used multiple
403 times around the application.
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}')
411 self.parse_as = parse_as
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
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
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))
428 provides = name if provides is None else provides
429 self.provides = provides
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)
445 self.display = display
447 # TODO: default? type check that it's a sequence matching min/max reqs
449 def __repr__(self):
450 return format_nonexp_repr(self, ['parse_as', 'min_count', 'max_count', 'display'])
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
459 def parse(self, posargs):
460 """Parse a list of strings as positional arguments.
462 Args:
463 posargs (list): List of strings, likely parsed by a Parser
464 instance from sys.argv.
466 Raises an ArgumentArityError if there are too many or too few
467 arguments.
469 Raises InvalidPositionalArgument if the argument doesn't match
470 the configured *parse_as*. See PosArgSpec for more info.
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'
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
511FLAGFILE_ENABLED = Flag('--flagfile', parse_as=str, multi='extend', missing=None, display=False, doc='')
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)
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))
538 return posargs
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.
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.
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 [])
578 self.posargs = _ensure_posargspec(posargs, 'posargs')
579 self.post_posargs = _ensure_posargspec(post_posargs, 'post_posargs')
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)
591 self.subprs_map = OrderedDict()
592 self._path_flag_map = OrderedDict()
593 self._path_flag_map[()] = OrderedDict()
595 for flag in flags:
596 self.add(flag)
597 if self.flagfile_flag:
598 self.add(self.flagfile_flag)
599 return
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])
606 def get_flags(self, path=(), with_hidden=True):
607 flag_map = self.get_flag_map(path=path, with_hidden=with_hidden)
609 return unique(flag_map.values())
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))
616 def _add_subparser(self, subprs):
617 """Process subcommand name, check for subcommand conflicts, check for
618 subcommand flag conflicts, then finally add subcommand.
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')
627 # validate that the subparser's name can be used as a subcommand
628 subprs_name = process_command_name(subprs.name)
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[()]
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}')
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
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
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?
660 def add(self, *a, **kw):
661 """Add a flag or subparser.
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.
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
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)
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))
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
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.
711 Args:
712 argv (list): A required list of strings. Pass ``None`` to
713 use ``sys.argv``.
715 This method may raise ArgumentParseError (or one of its
716 subtypes) if the list of strings fails to parse.
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.
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
750 # we record our progress as we parse to provide the most
751 # up-to-date info possible to the error and help handlers
753 try:
754 # then figure out the subcommand path
755 subcmds, args = self._parse_subcmds(args)
756 cpr.subcmds = tuple(subcmds)
758 prs = self.subprs_map[tuple(subcmds)] if subcmds else self
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))
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)
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)
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
782 parsed_post_posargs = prs.post_posargs.parse(post_posargs)
783 cpr.post_posargs = tuple(parsed_post_posargs)
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
791 return cpr
793 def _parse_subcmds(self, args):
794 """Expects arguments after the initial command (i.e., argv[1:])
796 Returns a tuple of (list_of_subcmds, remaining_args).
798 Raises on unknown subcommands."""
799 ret = []
801 for arg in args:
802 if arg.startswith('-'):
803 break # subcmd parsing complete
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):]
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:]
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)
844 return flag, arg_val, args[advance:]
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)
850 Returns a tuple of (multidict of flag names to parsed and validated values, remaining_args).
852 Raises on unknown subcommands.
853 """
854 flag_value_map = OMD()
855 ff_path_res_map = OrderedDict()
856 ff_path_seen = set()
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)
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)
875 return flag_value_map, ff_path_res_map, args
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)
903 if leftover_args:
904 raise ArgumentParseError('excessive flags or arguments for flag "%s",'
905 ' expected one flag per line' % flag.name)
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)
911 except FaceException as fe:
912 fe.args = (fe.args[0] + f' (on line {lineno} of flagfile "{path}")',)
913 raise
915 return ret
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 {}
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)
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
954def parse_sv_line(line, sep=','):
955 """Parse a single line of values, separated by the delimiter
956 *sep*. Supports quoting.
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
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
972 parsed = list(reader([line], dialect=_face_dialect))
973 return parsed[0]
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)::
981 --flag a1,b2,c3
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::
987 --flag 'a1,"b,2",c3'
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.
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.
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
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]
1019 __call__ = parse
1021 def __repr__(self):
1022 return format_exp_repr(self, ['parse_one_as'], ['sep', 'strip'])
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
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
1049 __call__ = parse
1051 def __repr__(self):
1052 return format_exp_repr(self, ['choices'], ['parse_as'])
1055class FilePathParam:
1056 """TODO
1058 ideas: exists, minimum permissions, can create, abspath, type=d/f
1059 (technically could also support socket, named pipe, and symlink)
1061 could do missing=TEMP, but that might be getting too fancy tbh.
1062 """
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 """