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
2import sys
3import shlex
4import codecs
5import os.path
6from collections import OrderedDict
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)
28try:
29 unicode
30except NameError:
31 unicode = str
34def _arg_to_subcmd(arg):
35 return arg.lower().replace('-', '_')
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]
45def _multi_extend(flag, arg_val_list):
46 "Return a list of all arguments specified for a flag"
47 ret = [v for v in arg_val_list if v is not flag.missing]
48 return ret
51def _multi_override(flag, arg_val_list):
52 "Return only the last argument specified for a flag"
53 return arg_val_list[-1]
55# TODO: _multi_ignore?
57_MULTI_SHORTCUTS = {'error': _multi_error,
58 False: _multi_error,
59 'extend': _multi_extend,
60 True: _multi_extend,
61 'override': _multi_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(object):
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_ = '%s -m %s' % (executable_path, pkg_name)
151 ret = {'args_': self,
152 'cmd_': cmd_,
153 'subcmds_': self.subcmds,
154 'flags_': self.flags,
155 'posargs_': self.posargs,
156 'post_posargs_': self.post_posargs,
157 'subcommand_': _subparser,
158 'command_': self.parser}
159 if self.flags:
160 ret.update(self.flags)
162 prs = self.parser if not self.subcmds else self.parser.subprs_map[self.subcmds]
163 if prs.posargs.provides:
164 posargs_provides = _posargs_to_provides(prs.posargs, self.posargs)
165 ret[prs.posargs.provides] = posargs_provides
166 if prs.post_posargs.provides:
167 posargs_provides = _posargs_to_provides(prs.posargs, self.post_posargs)
168 ret[prs.post_posargs.provides] = posargs_provides
170 return ret
172 def __repr__(self):
173 return format_nonexp_repr(self, ['name', 'argv', 'parser'])
176# TODO: allow name="--flag / -F" and do the split for automatic
177# char form?
178class Flag(object):
179 """The Flag object represents all there is to know about a resource
180 that can be parsed from argv and consumed by a Command
181 function. It also references a FlagDisplay, used by HelpHandlers
182 to control formatting of the flag during --help output
184 Args:
185 name (str): A string name for the flag, starting with a letter,
186 and consisting of only ASCII letters, numbers, '-', and '_'.
187 parse_as: How to interpret the flag. If *parse_as* is a
188 callable, it will be called with the argument to the flag,
189 the return value of which is stored in the parse result. If
190 *parse_as* is not a callable, then the flag takes no
191 argument, and the presence of the flag will produce this
192 value in the parse result. Defaults to ``str``, meaning a
193 default flag will take one string argument.
194 missing: How to interpret the absence of the flag. Can be any
195 value, which will be in the parse result when the flag is not
196 present. Can also be the special value ``face.ERROR``, which
197 will make the flag required. Defaults to ``None``.
198 multi (str): How to handle multiple instances of the same
199 flag. Pass 'overwrite' to accept the last flag's value. Pass
200 'extend' to collect all values into a list. Pass 'error' to
201 get the default behavior, which raises a DuplicateFlag
202 exception. *multi* can also take a callable, which accepts a
203 list of flag values and returns the value to be stored in the
204 :class:`CommandParseResult`.
205 char (str): A single-character short form for the flag. Can be
206 user-friendly for commonly-used flags. Defaults to ``None``.
207 doc (str): A summary of the flag's behavior, used in automatic
208 help generation.
209 display: Controls how the flag is displayed in automatic help
210 generation. Pass False to hide the flag, pass a string to
211 customize the label, and pass a FlagDisplay instance for full
212 customizability.
213 """
214 def __init__(self, name, parse_as=str, missing=None, multi='error',
215 char=None, doc=None, display=None):
216 self.name = flag_to_identifier(name)
217 self.doc = doc
218 self.parse_as = parse_as
219 self.missing = missing
220 if missing is ERROR and not callable(parse_as):
221 raise ValueError('cannot make an argument-less flag required.'
222 ' expected non-ERROR for missing, or a callable'
223 ' for parse_as, not: %r' % parse_as)
224 self.char = _validate_char(char) if char else None
226 if callable(multi):
227 self.multi = multi
228 elif multi in _MULTI_SHORTCUTS:
229 self.multi = _MULTI_SHORTCUTS[multi]
230 else:
231 raise ValueError('multi expected callable, bool, or one of %r, not: %r'
232 % (list(_MULTI_SHORTCUTS.keys()), multi))
234 self.set_display(display)
236 def set_display(self, display):
237 """Controls how the flag is displayed in automatic help
238 generation. Pass False to hide the flag, pass a string to
239 customize the label, and pass a FlagDisplay instance for full
240 customizability.
241 """
242 if display is None:
243 display = {}
244 elif isinstance(display, bool):
245 display = {'hidden': not display}
246 elif isinstance(display, str):
247 display = {'label': display}
248 if isinstance(display, dict):
249 display = FlagDisplay(self, **display)
250 if not isinstance(display, FlagDisplay):
251 raise TypeError('expected bool, text name, dict of display'
252 ' options, or FlagDisplay instance, not: %r'
253 % display)
254 self.display = display
256 def __repr__(self):
257 return format_nonexp_repr(self, ['name', 'parse_as'], ['missing', 'multi'],
258 opt_key=lambda v: v not in (None, _multi_error))
261class FlagDisplay(object):
262 """Provides individual overrides for most of a given flag's display
263 settings, as used by HelpFormatter instances attached to Parser
264 and Command objects. Pass an instance of this to
265 Flag.set_display() for full control of help output.
267 FlagDisplay instances are meant to be used 1:1 with Flag
268 instances, as they maintain a reference back to their associated
269 Flag. They are generally automatically created by a Flag
270 constructor, based on the "display" argument.
272 Args:
273 flag (Flag): The Flag instance to which this FlagDisplay applies.
274 label (str): The formatted version of the string used to
275 represent the flag in help and error messages. Defaults to
276 None, which allows the label to be autogenerated by the
277 HelpFormatter.
278 post_doc (str): An addendum string added to the Flag's own
279 doc. Defaults to a parenthetical describing whether the flag
280 takes an argument, and whether the argument is required.
281 full_doc (str): A string of the whole flag's doc, overriding
282 the doc + post_doc default.
283 value_name (str): For flags which take an argument, the string
284 to use as the placeholder of the flag argument in help and
285 error labels.
286 hidden (bool): Pass True to hide this flag in general help and
287 error messages. Defaults to False.
288 group: An integer or string indicating how this flag should be
289 grouped in help messages, improving readability. Integers are
290 unnamed groups, strings are for named groups. Defaults to 0.
291 sort_key: Flags are sorted in help output, pass an integer or
292 string to override the sort order.
294 """
295 # value_name -> arg_name?
296 def __init__(self, flag, **kw):
297 self.flag = flag
299 self.doc = flag.doc
300 if self.doc is None and callable(flag.parse_as):
301 _prep, desc = get_type_desc(flag.parse_as)
302 self.doc = 'Parsed with ' + desc
303 if _prep == 'as':
304 self.doc = desc
306 self.post_doc = kw.pop('post_doc', None)
307 self.full_doc = kw.pop('full_doc', None)
309 self.value_name = ''
310 if callable(flag.parse_as):
311 # TODO: use default when it's set and it's a basic renderable type
312 self.value_name = kw.pop('value_name', None) or self.flag.name.upper()
314 self.group = kw.pop('group', 0) # int or str
315 self._hide = kw.pop('hidden', False) # bool
316 self.label = kw.pop('label', None) # see hidden property below for more info
317 self.sort_key = kw.pop('sort_key', 0) # int or str
318 # TODO: sort_key is gonna need to be partitioned on type for py3
319 # TODO: maybe sort_key should be a counter so that flags sort
320 # in the order they are created
322 if kw:
323 raise TypeError('unexpected keyword arguments: %r' % kw.keys())
324 return
326 @property
327 def hidden(self):
328 return self._hide or self.label == ''
330 def __repr__(self):
331 return format_nonexp_repr(self, ['label', 'doc'], ['group', 'hidden'], opt_key=bool)
334class PosArgDisplay(object):
335 """Provides individual overrides for PosArgSpec display in automated
336 help formatting. Pass to a PosArgSpec constructor, which is in
337 turn passed to a Command/Parser.
339 Args:
340 spec (PosArgSpec): The associated PosArgSpec.
341 name (str): The string name of an individual positional
342 argument. Automatically pluralized in the label according to
343 PosArgSpec values. Defaults to 'arg'.
344 label (str): The full display label for positional arguments,
345 bypassing the automatic formatting of the *name* parameter.
346 doc (str): A summary description of the positional arguments.
347 post_doc (str): An informational addendum about the arguments,
348 often describes default behavior.
350 """
351 def __init__(self, **kw):
352 self.name = kw.pop('name', None) or 'arg'
353 self.doc = kw.pop('doc', '')
354 self.post_doc = kw.pop('post_doc', None)
355 self._hide = kw.pop('hidden', False) # bool
356 self.label = kw.pop('label', None)
358 if kw:
359 raise TypeError('unexpected keyword arguments: %r' % kw.keys())
360 return
362 @property
363 def hidden(self):
364 return self._hide or self.label == ''
366 def __repr__(self):
367 return format_nonexp_repr(self, ['name', 'label'])
370class PosArgSpec(object):
371 """Passed to Command/Parser as posargs and post_posargs parameters to
372 configure the number and type of positional arguments.
374 Args:
375 parse_as (callable): A function to call on each of the passed
376 arguments. Also accepts special argument ERROR, which will raise
377 an exception if positional arguments are passed. Defaults to str.
378 min_count (int): A minimimum number of positional
379 arguments. Defaults to 0.
380 max_count (int): A maximum number of positional arguments. Also
381 accepts None, meaning no maximum. Defaults to None.
382 display: Pass a string to customize the name in help output, or
383 False to hide it completely. Also accepts a PosArgDisplay
384 instance, or a dict of the respective arguments.
385 provides (str): name of an argument to be passed to a receptive
386 handler function.
387 name (str): A shortcut to set *display* name and *provides*
388 count (int): A shortcut to set min_count and max_count to a single value
389 when an exact number of arguments should be specified.
391 PosArgSpec instances are stateless and safe to be used multiple
392 times around the application.
394 """
395 def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None, **kwargs):
396 if not callable(parse_as) and parse_as is not ERROR:
397 raise TypeError('expected callable or ERROR for parse_as, not %r' % parse_as)
398 name = kwargs.pop('name', None)
399 count = kwargs.pop('count', None)
400 if kwargs:
401 raise TypeError('unexpected keyword arguments: %r' % list(kwargs.keys()))
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('expected min_count >= 0, not: %r' % self.min_count)
413 if self.max_count is not None and self.max_count <= 0:
414 raise ValueError('expected max_count > 0, not: %r' % self.max_count)
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('unexpected positional arguments: %r' % posargs)
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 = '%s argument' % min_count
473 if min_count > 1:
474 arg_range_text += 's'
475 else:
476 if min_count == 0:
477 arg_range_text = 'up to %s argument' % max_count
478 arg_range_text += 's' if (max_count and max_count > 1) else ''
479 elif max_count is None:
480 arg_range_text = 'at least %s argument' % min_count
481 arg_range_text += 's' if min_count > 1 else ''
482 else:
483 arg_range_text = '%s - %s arguments' % (min_count, max_count)
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(object):
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):
564 self.name = process_command_name(name)
565 self.doc = doc
566 flags = list(flags or [])
568 self.posargs = _ensure_posargspec(posargs, 'posargs')
569 self.post_posargs = _ensure_posargspec(post_posargs, 'post_posargs')
571 if flagfile is True:
572 self.flagfile_flag = FLAGFILE_ENABLED
573 elif isinstance(flagfile, Flag):
574 self.flagfile_flag = flagfile
575 elif not flagfile:
576 self.flagfile_flag = None
577 else:
578 raise TypeError('expected True, False, or Flag instance for'
579 ' flagfile, not: %r' % flagfile)
581 self.subprs_map = OrderedDict()
582 self._path_flag_map = OrderedDict()
583 self._path_flag_map[()] = OrderedDict()
585 for flag in flags:
586 self.add(flag)
587 if self.flagfile_flag:
588 self.add(self.flagfile_flag)
589 return
591 def get_flag_map(self, path, with_hidden=True):
592 flag_map = self._path_flag_map[path]
593 return OrderedDict([(k, f) for k, f in flag_map.items()
594 if with_hidden or not f.display.hidden])
596 def get_flags(self, path=(), with_hidden=True):
597 flag_map = self.get_flag_map(path=path, with_hidden=with_hidden)
599 return unique(flag_map.values())
601 def __repr__(self):
602 cn = self.__class__.__name__
603 return ('<%s name=%r subcmd_count=%r flag_count=%r posargs=%r>'
604 % (cn, self.name, len(self.subprs_map), len(self.get_flags()), self.posargs))
606 def _add_subparser(self, subprs):
607 """Process subcommand name, check for subcommand conflicts, check for
608 subcommand flag conflicts, then finally add subcommand.
610 To add a command under a different name, simply make a copy of
611 that parser or command with a different name.
612 """
613 if self.posargs.accepts_args:
614 raise ValueError('commands accepting positional arguments'
615 ' cannot take subcommands')
617 # validate that the subparser's name can be used as a subcommand
618 subprs_name = process_command_name(subprs.name)
620 # then, check for conflicts with existing subcommands and flags
621 for prs_path in self.subprs_map:
622 if prs_path[0] == subprs_name:
623 raise ValueError('conflicting subcommand name: %r' % subprs_name)
624 parent_flag_map = self._path_flag_map[()]
626 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True
627 for path, flags in subprs._path_flag_map.items():
628 if not check_no_conflicts(parent_flag_map, path, flags):
629 # TODO
630 raise ValueError('subcommand flags conflict with parent command: %r' % flags)
632 # with checks complete, add parser and all subparsers
633 self.subprs_map[(subprs_name,)] = subprs
634 for path, cur_subprs in list(subprs.subprs_map.items()):
635 new_path = (subprs_name,) + path
636 self.subprs_map[new_path] = cur_subprs
638 # Flags inherit down (a parent's flags are usable by the child)
639 for path, flags in subprs._path_flag_map.items():
640 new_flags = parent_flag_map.copy()
641 new_flags.update(flags)
642 self._path_flag_map[(subprs_name,) + path] = new_flags
644 # If two flags have the same name, as long as the "parse_as"
645 # is the same, things should be ok. Need to watch for
646 # overlapping aliases, too. This may allow subcommands to
647 # further document help strings. Should the same be allowed
648 # for defaults?
650 def add(self, *a, **kw):
651 """Add a flag or subparser.
653 Unless the first argument is a Parser or Flag object, the
654 arguments are the same as the Flag constructor, and will be
655 used to create a new Flag instance to be added.
657 May raise ValueError if arguments are not recognized as
658 Parser, Flag, or Flag parameters. ValueError may also be
659 raised on duplicate definitions and other conflicts.
660 """
661 if isinstance(a[0], Parser):
662 subprs = a[0]
663 self._add_subparser(subprs)
664 return
666 if isinstance(a[0], Flag):
667 flag = a[0]
668 else:
669 try:
670 flag = Flag(*a, **kw)
671 except TypeError as te:
672 raise ValueError('expected Parser, Flag, or Flag parameters,'
673 ' not: %r, %r (got %r)' % (a, kw, te))
674 return self._add_flag(flag)
676 def _add_flag(self, flag):
677 # first check there are no conflicts...
678 for subcmds, flag_map in self._path_flag_map.items():
679 conflict_flag = flag_map.get(flag.name) or (flag.char and flag_map.get(flag.char))
680 if conflict_flag is None:
681 continue
682 if flag.name in (conflict_flag.name, conflict_flag.char):
683 raise ValueError('pre-existing flag %r conflicts with name of new flag %r'
684 % (conflict_flag, flag.name))
685 if flag.char and flag.char in (conflict_flag.name, conflict_flag.char):
686 raise ValueError('pre-existing flag %r conflicts with short form for new flag %r'
687 % (conflict_flag, flag))
689 # ... then we add the flags
690 for flag_map in self._path_flag_map.values():
691 flag_map[flag.name] = flag
692 if flag.char:
693 flag_map[flag.char] = flag
694 return
696 def parse(self, argv):
697 """This method takes a list of strings and converts them into a
698 validated :class:`CommandParseResult` according to the flags,
699 subparsers, and other options configured.
701 Args:
702 argv (list): A required list of strings. Pass ``None`` to
703 use ``sys.argv``.
705 This method may raise ArgumentParseError (or one of its
706 subtypes) if the list of strings fails to parse.
708 .. note:: The *argv* parameter does not automatically default
709 to using ``sys.argv`` because it's best practice for
710 implementing codebases to perform that sort of
711 defaulting in their ``main()``, which should accept
712 an ``argv=None`` parameter. This simple step ensures
713 that the Python CLI application has some sort of
714 programmatic interface that doesn't require
715 subprocessing. See here for an example.
717 """
718 if argv is None:
719 argv = sys.argv
720 cpr = CommandParseResult(parser=self, argv=argv)
721 if not argv:
722 ape = ArgumentParseError('expected non-empty sequence of arguments, not: %r' % (argv,))
723 ape.prs_res = cpr
724 raise ape
725 for arg in argv:
726 if not isinstance(arg, (str, unicode)):
727 raise TypeError('parse expected all args as strings, not: %r (%s)' % (arg, type(arg).__name__))
728 '''
729 for subprs_path, subprs in self.subprs_map.items():
730 if len(subprs_path) == 1:
731 # _add_subparser takes care of recurring so we only
732 # need direct subparser descendants
733 self._add_subparser(subprs, overwrite=True)
734 '''
735 flag_map = None
736 # first snip off the first argument, the command itself
737 cmd_name, args = argv[0], list(argv)[1:]
738 cpr.name = cmd_name
740 # we record our progress as we parse to provide the most
741 # up-to-date info possible to the error and help handlers
743 try:
744 # then figure out the subcommand path
745 subcmds, args = self._parse_subcmds(args)
746 cpr.subcmds = tuple(subcmds)
748 prs = self.subprs_map[tuple(subcmds)] if subcmds else self
750 # then look up the subcommand's supported flags
751 # NOTE: get_flag_map() is used so that inheritors, like Command,
752 # can filter by actually-used arguments, not just
753 # available arguments.
754 cmd_flag_map = self.get_flag_map(path=tuple(subcmds))
756 # parse supported flags and validate their arguments
757 flag_map, flagfile_map, posargs = self._parse_flags(cmd_flag_map, args)
758 cpr.flags = OrderedDict(flag_map)
759 cpr.posargs = tuple(posargs)
761 # take care of dupes and check required flags
762 resolved_flag_map = self._resolve_flags(cmd_flag_map, flag_map, flagfile_map)
763 cpr.flags = OrderedDict(resolved_flag_map)
765 # separate out any trailing arguments from normal positional arguments
766 post_posargs = None # TODO: default to empty list?
767 parsed_post_posargs = None
768 if '--' in posargs:
769 posargs, post_posargs = split(posargs, '--', 1)
770 cpr.posargs, cpr.post_posargs = posargs, post_posargs
772 parsed_post_posargs = prs.post_posargs.parse(post_posargs)
773 cpr.post_posargs = tuple(parsed_post_posargs)
775 parsed_posargs = prs.posargs.parse(posargs)
776 cpr.posargs = tuple(parsed_posargs)
777 except ArgumentParseError as ape:
778 ape.prs_res = cpr
779 raise
781 return cpr
783 def _parse_subcmds(self, args):
784 """Expects arguments after the initial command (i.e., argv[1:])
786 Returns a tuple of (list_of_subcmds, remaining_args).
788 Raises on unknown subcommands."""
789 ret = []
791 for arg in args:
792 if arg.startswith('-'):
793 break # subcmd parsing complete
795 arg = _arg_to_subcmd(arg)
796 if tuple(ret + [arg]) not in self.subprs_map:
797 prs = self.subprs_map[tuple(ret)] if ret else self
798 if prs.posargs.parse_as is not ERROR or not prs.subprs_map:
799 # we actually have posargs from here
800 break
801 raise InvalidSubcommand.from_parse(prs, arg)
802 ret.append(arg)
803 return ret, args[len(ret):]
805 def _parse_single_flag(self, cmd_flag_map, args):
806 advance = 1
807 arg = args[0]
808 arg_text = None
809 try:
810 arg, arg_text = arg.split('=', maxsplit=1)
811 except ValueError:
812 pass
813 flag = cmd_flag_map.get(normalize_flag_name(arg))
814 if flag is None:
815 raise UnknownFlag.from_parse(cmd_flag_map, arg)
816 parse_as = flag.parse_as
817 if not callable(parse_as):
818 if arg_text:
819 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text)
820 # e.g., True is effectively store_true, False is effectively store_false
821 return flag, parse_as, args[1:]
823 try:
824 if arg_text is None:
825 arg_text = args[1]
826 advance = 2
827 except IndexError:
828 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg=None)
829 try:
830 arg_val = parse_as(arg_text)
831 except Exception as e:
832 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text, exc=e)
834 return flag, arg_val, args[advance:]
836 def _parse_flags(self, cmd_flag_map, args):
837 """Expects arguments after the initial command and subcommands (i.e.,
838 the second item returned from _parse_subcmds)
840 Returns a tuple of (multidict of flag names to parsed and validated values, remaining_args).
842 Raises on unknown subcommands.
843 """
844 flag_value_map = OMD()
845 ff_path_res_map = OrderedDict()
846 ff_path_seen = set()
848 orig_args = args
849 while args:
850 arg = args[0]
851 if not arg or arg[0] != '-' or arg == '-' or arg == '--':
852 # posargs or post_posargs beginning ('-' is a conventional pos arg for stdin)
853 break
854 flag, value, args = self._parse_single_flag(cmd_flag_map, args)
855 flag_value_map.add(flag.name, value)
857 if flag is self.flagfile_flag:
858 self._parse_flagfile(cmd_flag_map, value, res_map=ff_path_res_map)
859 for path, ff_flag_value_map in ff_path_res_map.items():
860 if path in ff_path_seen:
861 continue
862 flag_value_map.update_extend(ff_flag_value_map)
863 ff_path_seen.add(path)
865 return flag_value_map, ff_path_res_map, args
867 def _parse_flagfile(self, cmd_flag_map, path_or_file, res_map=None):
868 ret = res_map if res_map is not None else OrderedDict()
869 if callable(getattr(path_or_file, 'read', None)):
870 # enable StringIO and custom flagfile opening
871 f_name = getattr(path_or_file, 'name', None)
872 path = os.path.abspath(f_name) if f_name else repr(path_or_file)
873 ff_text = path_or_file.read()
874 else:
875 path = os.path.abspath(path_or_file)
876 try:
877 with codecs.open(path_or_file, 'r', 'utf-8') as f:
878 ff_text = f.read()
879 except (UnicodeError, EnvironmentError) as ee:
880 raise ArgumentParseError('failed to load flagfile "%s", got: %r' % (path, ee))
881 if path in res_map:
882 # we've already seen this file
883 return res_map
884 ret[path] = cur_file_res = OMD()
885 lines = ff_text.splitlines()
886 for lineno, line in enumerate(lines, 1):
887 try:
888 args = shlex.split(line, comments=True)
889 if not args:
890 continue # comment or empty line
891 flag, value, leftover_args = self._parse_single_flag(cmd_flag_map, args)
893 if leftover_args:
894 raise ArgumentParseError('excessive flags or arguments for flag "%s",'
895 ' expected one flag per line' % flag.name)
897 cur_file_res.add(flag.name, value)
898 if flag is self.flagfile_flag:
899 self._parse_flagfile(cmd_flag_map, value, res_map=ret)
901 except FaceException as fe:
902 fe.args = (fe.args[0] + ' (on line %s of flagfile "%s")' % (lineno, path),)
903 raise
905 return ret
907 def _resolve_flags(self, cmd_flag_map, parsed_flag_map, flagfile_map=None):
908 ret = OrderedDict()
909 cfm, pfm = cmd_flag_map, parsed_flag_map
910 flagfile_map = flagfile_map or {}
912 # check requireds and set defaults and then...
913 missing_flags = []
914 for flag_name, flag in cfm.items():
915 if flag.name in pfm:
916 continue
917 if flag.missing is ERROR:
918 missing_flags.append(flag.name)
919 else:
920 pfm[flag.name] = flag.missing
921 if missing_flags:
922 raise MissingRequiredFlags.from_parse(cfm, pfm, missing_flags)
924 # ... resolve dupes
925 for flag_name in pfm:
926 flag = cfm[flag_name]
927 arg_val_list = pfm.getlist(flag_name)
928 try:
929 ret[flag_name] = flag.multi(flag, arg_val_list)
930 except FaceException as fe:
931 ff_paths = []
932 for ff_path, ff_value_map in flagfile_map.items():
933 if flag_name in ff_value_map:
934 ff_paths.append(ff_path)
935 if ff_paths:
936 ff_label = 'flagfiles' if len(ff_paths) > 1 else 'flagfile'
937 msg = ('\n\t(check %s with definitions for flag "%s": %s)'
938 % (ff_label, flag_name, ', '.join(ff_paths)))
939 fe.args = (fe.args[0] + msg,)
940 raise
941 return ret
944def parse_sv_line(line, sep=','):
945 """Parse a single line of values, separated by the delimiter
946 *sep*. Supports quoting.
948 """
949 # TODO: this doesn't support unicode, which is intended to be
950 # handled at the layer above.
951 from csv import reader, Dialect, QUOTE_MINIMAL
953 class _face_dialect(Dialect):
954 delimiter = sep
955 escapechar = '\\'
956 quotechar = '"'
957 doublequote = True
958 skipinitialspace = False
959 lineterminator = '\n'
960 quoting = QUOTE_MINIMAL
962 parsed = list(reader([line], dialect=_face_dialect))
963 return parsed[0]
966class ListParam(object):
967 """The ListParam takes an argument as a character-separated list, and
968 produces a Python list of parsed values. Basically, the argument
969 equivalent of CSV (Comma-Separated Values)::
971 --flag a1,b2,c3
973 By default, this yields a ``['a1', 'b2', 'c3']`` as the value for
974 ``flag``. The format is also similar to CSV in that it supports
975 quoting when values themselves contain the separator::
977 --flag 'a1,"b,2",c3'
979 Args:
980 parse_one_as (callable): Turns a single value's text into its
981 parsed value.
982 sep (str): A single-character string representing the list
983 value separator. Defaults to ``,``.
984 strip (bool): Whether or not each value in the list should have
985 whitespace stripped before being passed to
986 *parse_one_as*. Defaults to False.
988 .. note:: Aside from using ListParam, an alternative method for
989 accepting multiple arguments is to use the
990 ``multi=True`` on the :class:`Flag` constructor. The
991 approach tends to be more verbose and can be confusing
992 because arguments can get spread across the command
993 line.
995 """
996 def __init__(self, parse_one_as=str, sep=',', strip=False):
997 # TODO: min/max limits?
998 self.parse_one_as = parse_one_as
999 self.sep = sep
1000 self.strip = strip
1002 def parse(self, list_text):
1003 "Parse a single string argument into a list of arguments."
1004 split_vals = parse_sv_line(list_text, self.sep)
1005 if self.strip:
1006 split_vals = [v.strip() for v in split_vals]
1007 return [self.parse_one_as(v) for v in split_vals]
1009 __call__ = parse
1011 def __repr__(self):
1012 return format_exp_repr(self, ['parse_one_as'], ['sep', 'strip'])
1015class ChoicesParam(object):
1016 """Parses a single value, limited to a set of *choices*. The actual
1017 converter used to parse is inferred from *choices* by default, but
1018 an explicit one can be set *parse_as*.
1019 """
1020 def __init__(self, choices, parse_as=None):
1021 if not choices:
1022 raise ValueError('expected at least one choice, not: %r' % choices)
1023 try:
1024 self.choices = sorted(choices)
1025 except Exception:
1026 # in case choices aren't sortable
1027 self.choices = list(choices)
1028 if parse_as is None:
1029 parse_as = type(self.choices[0])
1030 # TODO: check for builtins, raise if not a supported type
1031 self.parse_as = parse_as
1033 def parse(self, text):
1034 choice = self.parse_as(text)
1035 if choice not in self.choices:
1036 raise ArgumentParseError('expected one of %r, not: %r' % (self.choices, text))
1037 return choice
1039 __call__ = parse
1041 def __repr__(self):
1042 return format_exp_repr(self, ['choices'], ['parse_as'])
1045class FilePathParam(object):
1046 """TODO
1048 ideas: exists, minimum permissions, can create, abspath, type=d/f
1049 (technically could also support socket, named pipe, and symlink)
1051 could do missing=TEMP, but that might be getting too fancy tbh.
1052 """
1054class FileValueParam(object):
1055 """
1056 TODO: file with a single value in it, like a pidfile
1057 or a password file mounted in. Read in and treated like it
1058 was on the argv.
1059 """