Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/parser.py: 22%
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 codecs
4import os.path
5from collections import OrderedDict
6from typing import Optional
8from boltons.iterutils import split, unique
9from boltons.dictutils import OrderedMultiDict as OMD
10from boltons.funcutils import format_exp_repr, format_nonexp_repr
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)
29def _arg_to_subcmd(arg):
30 return arg.lower().replace('-', '_')
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]
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
46def _multi_override(flag, arg_val_list):
47 "Return only the last argument specified for a flag"
48 return arg_val_list[-1]
50# TODO: _multi_ignore?
52_MULTI_SHORTCUTS = {'error': _multi_error,
53 False: _multi_error,
54 'extend': _multi_extend,
55 True: _multi_extend,
56 'override': _multi_override}
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
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.
79 Cases as follows:
81 1. min_count > 1 or max_count > 1, pass through posargs as a list
82 2. max_count == 1 -> single argument or None
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)
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.
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 ``()``.
118 Instances of this class can be injected by accepting the ``args_``
119 builtin in their Command handler function.
121 """
122 def __init__(self, parser, argv=()):
123 self.parser = parser
124 self.argv = tuple(argv)
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
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
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 else:
146 cmd_ = get_minimal_executable(cmd_)
148 ret = {'args_': self,
149 'cmd_': cmd_,
150 'subcmds_': self.subcmds,
151 'flags_': self.flags,
152 'posargs_': self.posargs,
153 'post_posargs_': self.post_posargs,
154 'subcommand_': _subparser,
155 'command_': self.parser}
156 if self.flags:
157 ret.update(self.flags)
159 prs = self.parser if not self.subcmds else self.parser.subprs_map[self.subcmds]
160 if prs.posargs.provides:
161 posargs_provides = _posargs_to_provides(prs.posargs, self.posargs)
162 ret[prs.posargs.provides] = posargs_provides
163 if prs.post_posargs.provides:
164 posargs_provides = _posargs_to_provides(prs.posargs, self.post_posargs)
165 ret[prs.post_posargs.provides] = posargs_provides
167 return ret
169 def __repr__(self):
170 return format_nonexp_repr(self, ['name', 'argv', 'parser'])
173# TODO: allow name="--flag / -F" and do the split for automatic
174# char form?
175class Flag:
176 """The Flag object represents all there is to know about a resource
177 that can be parsed from argv and consumed by a Command
178 function. It also references a FlagDisplay, used by HelpHandlers
179 to control formatting of the flag during --help output
181 Args:
182 name (str): A string name for the flag, starting with a letter,
183 and consisting of only ASCII letters, numbers, '-', and '_'.
184 parse_as: How to interpret the flag. If *parse_as* is a
185 callable, it will be called with the argument to the flag,
186 the return value of which is stored in the parse result. If
187 *parse_as* is not a callable, then the flag takes no
188 argument, and the presence of the flag will produce this
189 value in the parse result. Defaults to ``str``, meaning a
190 default flag will take one string argument.
191 missing: How to interpret the absence of the flag. Can be any
192 value, which will be in the parse result when the flag is not
193 present. Can also be the special value ``face.ERROR``, which
194 will make the flag required. Defaults to ``None``.
195 multi (str): How to handle multiple instances of the same
196 flag. Pass 'overwrite' to accept the last flag's value. Pass
197 'extend' to collect all values into a list. Pass 'error' to
198 get the default behavior, which raises a DuplicateFlag
199 exception. *multi* can also take a callable, which accepts a
200 list of flag values and returns the value to be stored in the
201 :class:`CommandParseResult`.
202 char (str): A single-character short form for the flag. Can be
203 user-friendly for commonly-used flags. Defaults to ``None``.
204 doc (str): A summary of the flag's behavior, used in automatic
205 help generation.
206 display: Controls how the flag is displayed in automatic help
207 generation. Pass False to hide the flag, pass a string to
208 customize the label, and pass a FlagDisplay instance for full
209 customizability.
210 """
211 def __init__(self, name, parse_as=str, missing=None, multi='error',
212 char=None, doc=None, display=None):
213 self.name = flag_to_identifier(name)
214 self.doc = doc
215 self.parse_as = parse_as
216 self.missing = missing
217 if missing is ERROR and not callable(parse_as):
218 raise ValueError('cannot make an argument-less flag required.'
219 ' expected non-ERROR for missing, or a callable'
220 ' for parse_as, not: %r' % parse_as)
221 self.char = _validate_char(char) if char else None
223 if callable(multi):
224 self.multi = multi
225 elif multi in _MULTI_SHORTCUTS:
226 self.multi = _MULTI_SHORTCUTS[multi]
227 else:
228 raise ValueError('multi expected callable, bool, or one of %r, not: %r'
229 % (list(_MULTI_SHORTCUTS.keys()), multi))
231 self.set_display(display)
233 def set_display(self, display):
234 """Controls how the flag is displayed in automatic help
235 generation. Pass False to hide the flag, pass a string to
236 customize the label, and pass a FlagDisplay instance for full
237 customizability.
238 """
239 if display is None:
240 display = {}
241 elif isinstance(display, bool):
242 display = {'hidden': not display}
243 elif isinstance(display, str):
244 display = {'label': display}
245 if isinstance(display, dict):
246 display = FlagDisplay(self, **display)
247 if not isinstance(display, FlagDisplay):
248 raise TypeError('expected bool, text name, dict of display'
249 ' options, or FlagDisplay instance, not: %r'
250 % display)
251 self.display = display
253 def __repr__(self):
254 return format_nonexp_repr(self, ['name', 'parse_as'], ['missing', 'multi'],
255 opt_key=lambda v: v not in (None, _multi_error))
258class FlagDisplay:
259 """Provides individual overrides for most of a given flag's display
260 settings, as used by HelpFormatter instances attached to Parser
261 and Command objects. Pass an instance of this to
262 Flag.set_display() for full control of help output.
264 FlagDisplay instances are meant to be used 1:1 with Flag
265 instances, as they maintain a reference back to their associated
266 Flag. They are generally automatically created by a Flag
267 constructor, based on the "display" argument.
269 Args:
270 flag (Flag): The Flag instance to which this FlagDisplay applies.
271 label (str): The formatted version of the string used to
272 represent the flag in help and error messages. Defaults to
273 None, which allows the label to be autogenerated by the
274 HelpFormatter.
275 post_doc (str): An addendum string added to the Flag's own
276 doc. Defaults to a parenthetical describing whether the flag
277 takes an argument, and whether the argument is required.
278 full_doc (str): A string of the whole flag's doc, overriding
279 the doc + post_doc default.
280 value_name (str): For flags which take an argument, the string
281 to use as the placeholder of the flag argument in help and
282 error labels.
283 hidden (bool): Pass True to hide this flag in general help and
284 error messages. Defaults to False.
285 group: An integer or string indicating how this flag should be
286 grouped in help messages, improving readability. Integers are
287 unnamed groups, strings are for named groups. Defaults to 0.
288 sort_key: Flags are sorted in help output, pass an integer or
289 string to override the sort order.
291 """
292 # value_name -> arg_name?
293 def __init__(self, flag, *,
294 label: Optional[str] = None,
295 post_doc: Optional[str] = None,
296 full_doc: Optional[str] = None,
297 value_name: Optional[str] = None,
298 group: int = 0,
299 hidden: bool = False,
300 sort_key: int = 0):
301 self.flag = flag
303 self.doc = flag.doc
304 if self.doc is None and callable(flag.parse_as):
305 _prep, desc = get_type_desc(flag.parse_as)
306 self.doc = 'Parsed with ' + desc
307 if _prep == 'as':
308 self.doc = desc
310 self.post_doc = post_doc
311 self.full_doc = full_doc
313 self.value_name = ''
314 if callable(flag.parse_as):
315 # TODO: use default when it's set and it's a basic renderable type
316 self.value_name = value_name or self.flag.name.upper()
318 self.group = group
319 self._hide = hidden
320 self.label = label # see hidden property below for more info
321 self.sort_key = sort_key
322 # TODO: sort_key is gonna need to be partitioned on type for py3
323 # TODO: maybe sort_key should be a counter so that flags sort
324 # in the order they are created
325 return
327 @property
328 def hidden(self):
329 return self._hide or self.label == ''
331 def __repr__(self):
332 return format_nonexp_repr(self, ['label', 'doc'], ['group', 'hidden'], opt_key=bool)
335class PosArgDisplay:
336 """Provides individual overrides for PosArgSpec display in automated
337 help formatting. Pass to a PosArgSpec constructor, which is in
338 turn passed to a Command/Parser.
340 Args:
341 spec (PosArgSpec): The associated PosArgSpec.
342 name (str): The string name of an individual positional
343 argument. Automatically pluralized in the label according to
344 PosArgSpec values. Defaults to 'arg'.
345 label (str): The full display label for positional arguments,
346 bypassing the automatic formatting of the *name* parameter.
347 doc (str): A summary description of the positional arguments.
348 post_doc (str): An informational addendum about the arguments,
349 often describes default behavior.
351 """
352 def __init__(self, *,
353 name: Optional[str] = None,
354 doc: str = '',
355 post_doc: Optional[str] = None,
356 hidden: bool = False,
357 label: Optional[str] = None) -> None:
358 self.name = name or 'arg'
359 self.doc = doc
360 self.post_doc = post_doc
361 self._hide = hidden
362 self.label = label
364 @property
365 def hidden(self):
366 return self._hide or self.label == ''
368 def __repr__(self):
369 return format_nonexp_repr(self, ['name', 'label'])
372class PosArgSpec:
373 """Passed to Command/Parser as posargs and post_posargs parameters to
374 configure the number and type of positional arguments.
376 Args:
377 parse_as (callable): A function to call on each of the passed
378 arguments. Also accepts special argument ERROR, which will raise
379 an exception if positional arguments are passed. Defaults to str.
380 min_count (int): A minimimum number of positional
381 arguments. Defaults to 0.
382 max_count (int): A maximum number of positional arguments. Also
383 accepts None, meaning no maximum. Defaults to None.
384 display: Pass a string to customize the name in help output, or
385 False to hide it completely. Also accepts a PosArgDisplay
386 instance, or a dict of the respective arguments.
387 provides (str): name of an argument to be passed to a receptive
388 handler function.
389 name (str): A shortcut to set *display* name and *provides*
390 count (int): A shortcut to set min_count and max_count to a single value
391 when an exact number of arguments should be specified.
393 PosArgSpec instances are stateless and safe to be used multiple
394 times around the application.
396 """
397 def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None,
398 *, name: Optional[str] = None, count: Optional[int] = None):
399 if not callable(parse_as) and parse_as is not ERROR:
400 raise TypeError(f'expected callable or ERROR for parse_as, not {parse_as!r}')
402 self.parse_as = parse_as
404 # count convenience alias
405 min_count = count if min_count is None else min_count
406 max_count = count if max_count is None else max_count
408 self.min_count = int(min_count) if min_count else 0
409 self.max_count = int(max_count) if max_count is not None else None
411 if self.min_count < 0:
412 raise ValueError(f'expected min_count >= 0, not: {self.min_count!r}')
413 if self.max_count is not None and self.max_count <= 0:
414 raise ValueError(f'expected max_count > 0, not: {self.max_count!r}')
415 if self.max_count and self.min_count > self.max_count:
416 raise ValueError('expected min_count > max_count, not: %r > %r'
417 % (self.min_count, self.max_count))
419 provides = name if provides is None else provides
420 self.provides = provides
422 if display is None:
423 display = {}
424 elif isinstance(display, bool):
425 display = {'hidden': not display}
426 elif isinstance(display, str):
427 display = {'name': display}
428 if isinstance(display, dict):
429 display.setdefault('name', name)
430 display = PosArgDisplay(**display)
431 if not isinstance(display, PosArgDisplay):
432 raise TypeError('expected bool, text name, dict of display'
433 ' options, or PosArgDisplay instance, not: %r'
434 % display)
436 self.display = display
438 # TODO: default? type check that it's a sequence matching min/max reqs
440 def __repr__(self):
441 return format_nonexp_repr(self, ['parse_as', 'min_count', 'max_count', 'display'])
443 @property
444 def accepts_args(self):
445 """True if this PosArgSpec is configured to accept one or
446 more arguments.
447 """
448 return self.parse_as is not ERROR
450 def parse(self, posargs):
451 """Parse a list of strings as positional arguments.
453 Args:
454 posargs (list): List of strings, likely parsed by a Parser
455 instance from sys.argv.
457 Raises an ArgumentArityError if there are too many or too few
458 arguments.
460 Raises InvalidPositionalArgument if the argument doesn't match
461 the configured *parse_as*. See PosArgSpec for more info.
463 Returns a list of arguments, parsed with *parse_as*.
464 """
465 len_posargs = len(posargs)
466 if posargs and not self.accepts_args:
467 # TODO: check for likely subcommands
468 raise ArgumentArityError(f'unexpected positional arguments: {posargs!r}')
469 min_count, max_count = self.min_count, self.max_count
470 if min_count == max_count:
471 # min_count must be >0 because max_count cannot be 0
472 arg_range_text = f'{min_count} argument'
473 if min_count > 1:
474 arg_range_text += 's'
475 else:
476 if min_count == 0:
477 arg_range_text = f'up to {max_count} argument'
478 arg_range_text += 's' if (max_count and max_count > 1) else ''
479 elif max_count is None:
480 arg_range_text = f'at least {min_count} argument'
481 arg_range_text += 's' if min_count > 1 else ''
482 else:
483 arg_range_text = f'{min_count} - {max_count} arguments'
485 if len_posargs < min_count:
486 raise ArgumentArityError('too few arguments, expected %s, got %s'
487 % (arg_range_text, len_posargs))
488 if max_count is not None and len_posargs > max_count:
489 raise ArgumentArityError('too many arguments, expected %s, got %s'
490 % (arg_range_text, len_posargs))
491 ret = []
492 for pa in posargs:
493 try:
494 val = self.parse_as(pa)
495 except Exception as exc:
496 raise InvalidPositionalArgument.from_parse(self, pa, exc)
497 else:
498 ret.append(val)
499 return ret
502FLAGFILE_ENABLED = Flag('--flagfile', parse_as=str, multi='extend', missing=None, display=False, doc='')
505def _ensure_posargspec(posargs, posargs_name):
506 if not posargs:
507 # take no posargs
508 posargs = PosArgSpec(parse_as=ERROR)
509 elif posargs is True:
510 # take any number of posargs
511 posargs = PosArgSpec()
512 elif isinstance(posargs, int):
513 # take an exact number of posargs
514 # (True and False are handled above, so only real nonzero ints get here)
515 posargs = PosArgSpec(min_count=posargs, max_count=posargs)
516 elif isinstance(posargs, str):
517 posargs = PosArgSpec(display=posargs, provides=posargs)
518 elif isinstance(posargs, dict):
519 posargs = PosArgSpec(**posargs)
520 elif callable(posargs):
521 # take any number of posargs of a given format
522 posargs = PosArgSpec(parse_as=posargs)
524 if not isinstance(posargs, PosArgSpec):
525 raise TypeError('expected %s as True, False, number of args, text name of args,'
526 ' dict of PosArgSpec options, or instance of PosArgSpec, not: %r'
527 % (posargs_name, posargs))
529 return posargs
532class Parser:
533 """The Parser lies at the center of face, primarily providing a
534 configurable validation logic on top of the conventional grammar
535 for CLI argument parsing.
537 Args:
538 name (str): A name used to identify this command. Important
539 when the command is embedded as a subcommand of another
540 command.
541 doc (str): An optional summary description of the command, used
542 to generate help and usage information.
543 flags (list): A list of Flag instances. Optional, as flags can
544 be added with :meth:`~Parser.add()`.
545 posargs (bool): Defaults to disabled, pass ``True`` to enable
546 the Parser to accept positional arguments. Pass a callable
547 to parse the positional arguments using that
548 function/type. Pass a :class:`PosArgSpec` for full
549 customizability.
550 post_posargs (bool): Same as *posargs*, but refers to the list
551 of arguments following the ``--`` conventional marker. See
552 ``git`` and ``tox`` for examples of commands using this
553 style of positional argument.
554 flagfile (bool): Defaults to enabled, pass ``False`` to disable
555 flagfile support. Pass a :class:`Flag` instance to use a
556 custom flag instead of ``--flagfile``. Read more about
557 Flagfiles below.
559 Once initialized, parsing is performed by calling
560 :meth:`Parser.parse()` with ``sys.argv`` or any other list of strings.
561 """
562 def __init__(self, name, doc=None, flags=None, posargs=None,
563 post_posargs=None, flagfile=True, group=None):
564 self.name = process_command_name(name)
565 self.doc = doc
566 self.group = group
567 flags = list(flags or [])
569 self.posargs = _ensure_posargspec(posargs, 'posargs')
570 self.post_posargs = _ensure_posargspec(post_posargs, 'post_posargs')
572 if flagfile is True:
573 self.flagfile_flag = FLAGFILE_ENABLED
574 elif isinstance(flagfile, Flag):
575 self.flagfile_flag = flagfile
576 elif not flagfile:
577 self.flagfile_flag = None
578 else:
579 raise TypeError('expected True, False, or Flag instance for'
580 ' flagfile, not: %r' % flagfile)
582 self.subprs_map = OrderedDict()
583 self._path_flag_map = OrderedDict()
584 self._path_flag_map[()] = OrderedDict()
586 for flag in flags:
587 self.add(flag)
588 if self.flagfile_flag:
589 self.add(self.flagfile_flag)
590 return
592 def get_flag_map(self, path, with_hidden=True):
593 flag_map = self._path_flag_map[path]
594 return OrderedDict([(k, f) for k, f in flag_map.items()
595 if with_hidden or not f.display.hidden])
597 def get_flags(self, path=(), with_hidden=True):
598 flag_map = self.get_flag_map(path=path, with_hidden=with_hidden)
600 return unique(flag_map.values())
602 def __repr__(self):
603 cn = self.__class__.__name__
604 return ('<%s name=%r subcmd_count=%r flag_count=%r posargs=%r>'
605 % (cn, self.name, len(self.subprs_map), len(self.get_flags()), self.posargs))
607 def _add_subparser(self, subprs):
608 """Process subcommand name, check for subcommand conflicts, check for
609 subcommand flag conflicts, then finally add subcommand.
611 To add a command under a different name, simply make a copy of
612 that parser or command with a different name.
613 """
614 if self.posargs.accepts_args:
615 raise ValueError('commands accepting positional arguments'
616 ' cannot take subcommands')
618 # validate that the subparser's name can be used as a subcommand
619 subprs_name = process_command_name(subprs.name)
621 # then, check for conflicts with existing subcommands and flags
622 for prs_path in self.subprs_map:
623 if prs_path[0] == subprs_name:
624 raise ValueError(f'conflicting subcommand name: {subprs_name!r}')
625 parent_flag_map = self._path_flag_map[()]
627 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True
628 for path, flags in subprs._path_flag_map.items():
629 if not check_no_conflicts(parent_flag_map, path, flags):
630 # TODO
631 raise ValueError(f'subcommand flags conflict with parent command: {flags!r}')
633 # with checks complete, add parser and all subparsers
634 self.subprs_map[(subprs_name,)] = subprs
635 for path, cur_subprs in list(subprs.subprs_map.items()):
636 new_path = (subprs_name,) + path
637 self.subprs_map[new_path] = cur_subprs
639 # Flags inherit down (a parent's flags are usable by the child)
640 for path, flags in subprs._path_flag_map.items():
641 new_flags = parent_flag_map.copy()
642 new_flags.update(flags)
643 self._path_flag_map[(subprs_name,) + path] = new_flags
645 # If two flags have the same name, as long as the "parse_as"
646 # is the same, things should be ok. Need to watch for
647 # overlapping aliases, too. This may allow subcommands to
648 # further document help strings. Should the same be allowed
649 # for defaults?
651 def add(self, *a, **kw):
652 """Add a flag or subparser.
654 Unless the first argument is a Parser or Flag object, the
655 arguments are the same as the Flag constructor, and will be
656 used to create a new Flag instance to be added.
658 May raise ValueError if arguments are not recognized as
659 Parser, Flag, or Flag parameters. ValueError may also be
660 raised on duplicate definitions and other conflicts.
661 """
662 if isinstance(a[0], Parser):
663 subprs = a[0]
664 self._add_subparser(subprs)
665 return
667 if isinstance(a[0], Flag):
668 flag = a[0]
669 else:
670 try:
671 flag = Flag(*a, **kw)
672 except TypeError as te:
673 raise ValueError('expected Parser, Flag, or Flag parameters,'
674 ' not: %r, %r (got %r)' % (a, kw, te))
675 return self._add_flag(flag)
677 def _add_flag(self, flag):
678 # first check there are no conflicts...
679 for subcmds, flag_map in self._path_flag_map.items():
680 conflict_flag = flag_map.get(flag.name) or (flag.char and flag_map.get(flag.char))
681 if conflict_flag is None:
682 continue
683 if flag.name in (conflict_flag.name, conflict_flag.char):
684 raise ValueError('pre-existing flag %r conflicts with name of new flag %r'
685 % (conflict_flag, flag.name))
686 if flag.char and flag.char in (conflict_flag.name, conflict_flag.char):
687 raise ValueError('pre-existing flag %r conflicts with short form for new flag %r'
688 % (conflict_flag, flag))
690 # ... then we add the flags
691 for flag_map in self._path_flag_map.values():
692 flag_map[flag.name] = flag
693 if flag.char:
694 flag_map[flag.char] = flag
695 return
697 def parse(self, argv):
698 """This method takes a list of strings and converts them into a
699 validated :class:`CommandParseResult` according to the flags,
700 subparsers, and other options configured.
702 Args:
703 argv (list): A required list of strings. Pass ``None`` to
704 use ``sys.argv``.
706 This method may raise ArgumentParseError (or one of its
707 subtypes) if the list of strings fails to parse.
709 .. note:: The *argv* parameter does not automatically default
710 to using ``sys.argv`` because it's best practice for
711 implementing codebases to perform that sort of
712 defaulting in their ``main()``, which should accept
713 an ``argv=None`` parameter. This simple step ensures
714 that the Python CLI application has some sort of
715 programmatic interface that doesn't require
716 subprocessing. See here for an example.
718 """
719 if argv is None:
720 argv = sys.argv
721 cpr = CommandParseResult(parser=self, argv=argv)
722 if not argv:
723 ape = ArgumentParseError(f'expected non-empty sequence of arguments, not: {argv!r}')
724 ape.prs_res = cpr
725 raise ape
726 for arg in argv:
727 if not isinstance(arg, str):
728 raise TypeError(f'parse expected all args as strings, not: {arg!r} ({type(arg).__name__})')
729 '''
730 for subprs_path, subprs in self.subprs_map.items():
731 if len(subprs_path) == 1:
732 # _add_subparser takes care of recurring so we only
733 # need direct subparser descendants
734 self._add_subparser(subprs, overwrite=True)
735 '''
736 flag_map = None
737 # first snip off the first argument, the command itself
738 cmd_name, args = argv[0], list(argv)[1:]
739 cpr.name = cmd_name
741 # we record our progress as we parse to provide the most
742 # up-to-date info possible to the error and help handlers
744 try:
745 # then figure out the subcommand path
746 subcmds, args = self._parse_subcmds(args)
747 cpr.subcmds = tuple(subcmds)
749 prs = self.subprs_map[tuple(subcmds)] if subcmds else self
751 # then look up the subcommand's supported flags
752 # NOTE: get_flag_map() is used so that inheritors, like Command,
753 # can filter by actually-used arguments, not just
754 # available arguments.
755 cmd_flag_map = self.get_flag_map(path=tuple(subcmds))
757 # parse supported flags and validate their arguments
758 flag_map, flagfile_map, posargs = self._parse_flags(cmd_flag_map, args)
759 cpr.flags = OrderedDict(flag_map)
760 cpr.posargs = tuple(posargs)
762 # take care of dupes and check required flags
763 resolved_flag_map = self._resolve_flags(cmd_flag_map, flag_map, flagfile_map)
764 cpr.flags = OrderedDict(resolved_flag_map)
766 # separate out any trailing arguments from normal positional arguments
767 post_posargs = None # TODO: default to empty list?
768 parsed_post_posargs = None
769 if '--' in posargs:
770 posargs, post_posargs = split(posargs, '--', 1)
771 cpr.posargs, cpr.post_posargs = posargs, post_posargs
773 parsed_post_posargs = prs.post_posargs.parse(post_posargs)
774 cpr.post_posargs = tuple(parsed_post_posargs)
776 parsed_posargs = prs.posargs.parse(posargs)
777 cpr.posargs = tuple(parsed_posargs)
778 except ArgumentParseError as ape:
779 ape.prs_res = cpr
780 raise
782 return cpr
784 def _parse_subcmds(self, args):
785 """Expects arguments after the initial command (i.e., argv[1:])
787 Returns a tuple of (list_of_subcmds, remaining_args).
789 Raises on unknown subcommands."""
790 ret = []
792 for arg in args:
793 if arg.startswith('-'):
794 break # subcmd parsing complete
796 arg = _arg_to_subcmd(arg)
797 if tuple(ret + [arg]) not in self.subprs_map:
798 prs = self.subprs_map[tuple(ret)] if ret else self
799 if prs.posargs.parse_as is not ERROR or not prs.subprs_map:
800 # we actually have posargs from here
801 break
802 raise InvalidSubcommand.from_parse(prs, arg)
803 ret.append(arg)
804 return ret, args[len(ret):]
806 def _parse_single_flag(self, cmd_flag_map, args):
807 advance = 1
808 arg = args[0]
809 arg_text = None
810 try:
811 arg, arg_text = arg.split('=', maxsplit=1)
812 except ValueError:
813 pass
814 flag = cmd_flag_map.get(normalize_flag_name(arg))
815 if flag is None:
816 raise UnknownFlag.from_parse(cmd_flag_map, arg)
817 parse_as = flag.parse_as
818 if not callable(parse_as):
819 if arg_text:
820 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text)
821 # e.g., True is effectively store_true, False is effectively store_false
822 return flag, parse_as, args[1:]
824 try:
825 if arg_text is None:
826 arg_text = args[1]
827 advance = 2
828 except IndexError:
829 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg=None)
830 try:
831 arg_val = parse_as(arg_text)
832 except Exception as e:
833 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text, exc=e)
835 return flag, arg_val, args[advance:]
837 def _parse_flags(self, cmd_flag_map, args):
838 """Expects arguments after the initial command and subcommands (i.e.,
839 the second item returned from _parse_subcmds)
841 Returns a tuple of (multidict of flag names to parsed and validated values, remaining_args).
843 Raises on unknown subcommands.
844 """
845 flag_value_map = OMD()
846 ff_path_res_map = OrderedDict()
847 ff_path_seen = set()
849 orig_args = args
850 while args:
851 arg = args[0]
852 if not arg or arg[0] != '-' or arg == '-' or arg == '--':
853 # posargs or post_posargs beginning ('-' is a conventional pos arg for stdin)
854 break
855 flag, value, args = self._parse_single_flag(cmd_flag_map, args)
856 flag_value_map.add(flag.name, value)
858 if flag is self.flagfile_flag:
859 self._parse_flagfile(cmd_flag_map, value, res_map=ff_path_res_map)
860 for path, ff_flag_value_map in ff_path_res_map.items():
861 if path in ff_path_seen:
862 continue
863 flag_value_map.update_extend(ff_flag_value_map)
864 ff_path_seen.add(path)
866 return flag_value_map, ff_path_res_map, args
868 def _parse_flagfile(self, cmd_flag_map, path_or_file, res_map=None):
869 ret = res_map if res_map is not None else OrderedDict()
870 if callable(getattr(path_or_file, 'read', None)):
871 # enable StringIO and custom flagfile opening
872 f_name = getattr(path_or_file, 'name', None)
873 path = os.path.abspath(f_name) if f_name else repr(path_or_file)
874 ff_text = path_or_file.read()
875 else:
876 path = os.path.abspath(path_or_file)
877 try:
878 with codecs.open(path_or_file, 'r', 'utf-8') as f:
879 ff_text = f.read()
880 except (UnicodeError, OSError) as ee:
881 raise ArgumentParseError(f'failed to load flagfile "{path}", got: {ee!r}')
882 if path in res_map:
883 # we've already seen this file
884 return res_map
885 ret[path] = cur_file_res = OMD()
886 lines = ff_text.splitlines()
887 for lineno, line in enumerate(lines, 1):
888 try:
889 args = shlex.split(line, comments=True)
890 if not args:
891 continue # comment or empty line
892 flag, value, leftover_args = self._parse_single_flag(cmd_flag_map, args)
894 if leftover_args:
895 raise ArgumentParseError('excessive flags or arguments for flag "%s",'
896 ' expected one flag per line' % flag.name)
898 cur_file_res.add(flag.name, value)
899 if flag is self.flagfile_flag:
900 self._parse_flagfile(cmd_flag_map, value, res_map=ret)
902 except FaceException as fe:
903 fe.args = (fe.args[0] + f' (on line {lineno} of flagfile "{path}")',)
904 raise
906 return ret
908 def _resolve_flags(self, cmd_flag_map, parsed_flag_map, flagfile_map=None):
909 ret = OrderedDict()
910 cfm, pfm = cmd_flag_map, parsed_flag_map
911 flagfile_map = flagfile_map or {}
913 # check requireds and set defaults and then...
914 missing_flags = []
915 for flag_name, flag in cfm.items():
916 if flag.name in pfm:
917 continue
918 if flag.missing is ERROR:
919 missing_flags.append(flag.name)
920 else:
921 pfm[flag.name] = flag.missing
922 if missing_flags:
923 raise MissingRequiredFlags.from_parse(cfm, pfm, missing_flags)
925 # ... resolve dupes
926 for flag_name in pfm:
927 flag = cfm[flag_name]
928 arg_val_list = pfm.getlist(flag_name)
929 try:
930 ret[flag_name] = flag.multi(flag, arg_val_list)
931 except FaceException as fe:
932 ff_paths = []
933 for ff_path, ff_value_map in flagfile_map.items():
934 if flag_name in ff_value_map:
935 ff_paths.append(ff_path)
936 if ff_paths:
937 ff_label = 'flagfiles' if len(ff_paths) > 1 else 'flagfile'
938 msg = ('\n\t(check %s with definitions for flag "%s": %s)'
939 % (ff_label, flag_name, ', '.join(ff_paths)))
940 fe.args = (fe.args[0] + msg,)
941 raise
942 return ret
945def parse_sv_line(line, sep=','):
946 """Parse a single line of values, separated by the delimiter
947 *sep*. Supports quoting.
949 """
950 # TODO: this doesn't support unicode, which is intended to be
951 # handled at the layer above.
952 from csv import reader, Dialect, QUOTE_MINIMAL
954 class _face_dialect(Dialect):
955 delimiter = sep
956 escapechar = '\\'
957 quotechar = '"'
958 doublequote = True
959 skipinitialspace = False
960 lineterminator = '\n'
961 quoting = QUOTE_MINIMAL
963 parsed = list(reader([line], dialect=_face_dialect))
964 return parsed[0]
967class ListParam:
968 """The ListParam takes an argument as a character-separated list, and
969 produces a Python list of parsed values. Basically, the argument
970 equivalent of CSV (Comma-Separated Values)::
972 --flag a1,b2,c3
974 By default, this yields a ``['a1', 'b2', 'c3']`` as the value for
975 ``flag``. The format is also similar to CSV in that it supports
976 quoting when values themselves contain the separator::
978 --flag 'a1,"b,2",c3'
980 Args:
981 parse_one_as (callable): Turns a single value's text into its
982 parsed value.
983 sep (str): A single-character string representing the list
984 value separator. Defaults to ``,``.
985 strip (bool): Whether or not each value in the list should have
986 whitespace stripped before being passed to
987 *parse_one_as*. Defaults to False.
989 .. note:: Aside from using ListParam, an alternative method for
990 accepting multiple arguments is to use the
991 ``multi=True`` on the :class:`Flag` constructor. The
992 approach tends to be more verbose and can be confusing
993 because arguments can get spread across the command
994 line.
996 """
997 def __init__(self, parse_one_as=str, sep=',', strip=False):
998 # TODO: min/max limits?
999 self.parse_one_as = parse_one_as
1000 self.sep = sep
1001 self.strip = strip
1003 def parse(self, list_text):
1004 "Parse a single string argument into a list of arguments."
1005 split_vals = parse_sv_line(list_text, self.sep)
1006 if self.strip:
1007 split_vals = [v.strip() for v in split_vals]
1008 return [self.parse_one_as(v) for v in split_vals]
1010 __call__ = parse
1012 def __repr__(self):
1013 return format_exp_repr(self, ['parse_one_as'], ['sep', 'strip'])
1016class ChoicesParam:
1017 """Parses a single value, limited to a set of *choices*. The actual
1018 converter used to parse is inferred from *choices* by default, but
1019 an explicit one can be set *parse_as*.
1020 """
1021 def __init__(self, choices, parse_as=None):
1022 if not choices:
1023 raise ValueError(f'expected at least one choice, not: {choices!r}')
1024 try:
1025 self.choices = sorted(choices)
1026 except Exception:
1027 # in case choices aren't sortable
1028 self.choices = list(choices)
1029 if parse_as is None:
1030 parse_as = type(self.choices[0])
1031 # TODO: check for builtins, raise if not a supported type
1032 self.parse_as = parse_as
1034 def parse(self, text):
1035 choice = self.parse_as(text)
1036 if choice not in self.choices:
1037 raise ArgumentParseError(f'expected one of {self.choices!r}, not: {text!r}')
1038 return choice
1040 __call__ = parse
1042 def __repr__(self):
1043 return format_exp_repr(self, ['choices'], ['parse_as'])
1046class FilePathParam:
1047 """TODO
1049 ideas: exists, minimum permissions, can create, abspath, type=d/f
1050 (technically could also support socket, named pipe, and symlink)
1052 could do missing=TEMP, but that might be getting too fancy tbh.
1053 """
1055class FileValueParam:
1056 """
1057 TODO: file with a single value in it, like a pidfile
1058 or a password file mounted in. Read in and treated like it
1059 was on the argv.
1060 """