Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/face/parser.py: 23%
494 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:23 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:23 +0000
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 return arg_val_list
50def _multi_override(flag, arg_val_list):
51 "Return only the last argument specified for a flag"
52 return arg_val_list[-1]
54# TODO: _multi_ignore?
56_MULTI_SHORTCUTS = {'error': _multi_error,
57 False: _multi_error,
58 'extend': _multi_extend,
59 True: _multi_extend,
60 'override': _multi_override}
63_VALID_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!*+./?@_'
64def _validate_char(char):
65 orig_char = char
66 if char[0] == '-' and len(char) > 1:
67 char = char[1:]
68 if len(char) > 1:
69 raise ValueError('char flags must be exactly one character, optionally'
70 ' prefixed by a dash, not: %r' % orig_char)
71 if char not in _VALID_CHARS:
72 raise ValueError('expected valid flag character (ASCII letters, numbers,'
73 ' or shell-compatible punctuation), not: %r' % orig_char)
74 return char
77def _posargs_to_provides(posargspec, posargs):
78 '''Automatically unwrap injectable posargs into a more intuitive
79 format, similar to an API a human might design. For instance, a
80 function which takes exactly one argument would not take a list of
81 exactly one argument.
83 Cases as follows:
85 1. min_count > 1 or max_count > 1, pass through posargs as a list
86 2. max_count == 1 -> single argument or None
88 Even if min_count == 1, you can get a None back. This compromise
89 was made necessary to keep "to_cmd_scope" robust enough to pass to
90 help/error handler funcs when validation fails.
91 '''
92 # all of the following assumes a valid posargspec, with min_count
93 # <= max_count, etc.
94 pas = posargspec
95 if pas.max_count is None or pas.min_count > 1 or pas.max_count > 1:
96 return posargs
97 if pas.max_count == 1:
98 # None is considered sufficiently unambiguous, even for cases when pas.min_count==1
99 return posargs[0] if posargs else None
100 raise RuntimeError('invalid posargspec/posargs configuration %r -- %r'
101 % (posargspec, posargs)) # pragma: no cover (shouldn't get here)
104class CommandParseResult(object):
105 """The result of :meth:`Parser.parse`, instances of this type
106 semantically store all that a command line can contain. Each
107 argument corresponds 1:1 with an attribute.
109 Args:
110 name (str): Top-level program name, typically the first
111 argument on the command line, i.e., ``sys.argv[0]``.
112 subcmds (tuple): Sequence of subcommand names.
113 flags (OrderedDict): Mapping of canonical flag names to matched values.
114 posargs (tuple): Sequence of parsed positional arguments.
115 post_posargs (tuple): Sequence of parsed post-positional
116 arguments (args following ``--``)
117 parser (Parser): The Parser instance that parsed this
118 result. Defaults to None.
119 argv (tuple): The sequence of strings parsed by the Parser to
120 yield this result. Defaults to ``()``.
122 Instances of this class can be injected by accepting the "args_"
123 builtin in their Command handler function.
125 """
126 def __init__(self, parser, argv=()):
127 self.parser = parser
128 self.argv = tuple(argv)
130 self.name = None # str
131 self.subcmds = None # tuple
132 self.flags = None # OrderedDict
133 self.posargs = None # tuple
134 self.post_posargs = None # tuple
136 def to_cmd_scope(self):
137 "returns a dict which can be used as kwargs in an inject call"
138 _subparser = self.parser.subprs_map[self.subcmds] if self.subcmds else self.parser
140 if not self.argv:
141 cmd_ = self.parser.name
142 else:
143 cmd_ = self.argv[0]
144 path, basename = os.path.split(cmd_)
145 if basename == '__main__.py':
146 pkg_name = os.path.basename(path)
147 executable_path = get_minimal_executable()
148 cmd_ = '%s -m %s' % (executable_path, pkg_name)
150 ret = {'args_': self,
151 'cmd_': cmd_,
152 'subcmds_': self.subcmds,
153 'flags_': self.flags,
154 'posargs_': self.posargs,
155 'post_posargs_': self.post_posargs,
156 'subcommand_': _subparser,
157 'command_': self.parser}
158 if self.flags:
159 ret.update(self.flags)
161 prs = self.parser if not self.subcmds else self.parser.subprs_map[self.subcmds]
162 if prs.posargs.provides:
163 posargs_provides = _posargs_to_provides(prs.posargs, self.posargs)
164 ret[prs.posargs.provides] = posargs_provides
165 if prs.post_posargs.provides:
166 posargs_provides = _posargs_to_provides(prs.posargs, self.post_posargs)
167 ret[prs.post_posargs.provides] = posargs_provides
169 return ret
171 def __repr__(self):
172 return format_nonexp_repr(self, ['name', 'argv', 'parser'])
175# TODO: allow name="--flag / -F" and do the split for automatic
176# char form?
177class Flag(object):
178 """The Flag object represents all there is to know about a resource
179 that can be parsed from argv and consumed by a Command
180 function. It also references a FlagDisplay, used by HelpHandlers
181 to control formatting of the flag during --help output
183 Args:
184 name (str): A string name for the flag, starting with a letter,
185 and consisting of only ASCII letters, numbers, '-', and '_'.
186 parse_as: How to interpret the flag. If *parse_as* is a
187 callable, it will be called with the argument to the flag,
188 the return value of which is stored in the parse result. If
189 *parse_as* is not a callable, then the flag takes no
190 argument, and the presence of the flag will produce this
191 value in the parse result. Defaults to ``str``, meaning a
192 default flag will take one string argument.
193 missing: How to interpret the absence of the flag. Can be any
194 value, which will be in the parse result when the flag is not
195 present. Can also be the special value ``face.ERROR``, which
196 will make the flag required. Defaults to ``None``.
197 multi (str): How to handle multiple instances of the same
198 flag. Pass 'overwrite' to accept the last flag's value. Pass
199 'extend' to collect all values into a list. Pass 'error' to
200 get the default behavior, which raises a DuplicateFlag
201 exception. *multi* can also take a callable, which accepts a
202 list of flag values and returns the value to be stored in the
203 :class:`CommandParseResult`.
204 char (str): A single-character short form for the flag. Can be
205 user-friendly for commonly-used flags. Defaults to ``None``.
206 doc (str): A summary of the flag's behavior, used in automatic
207 help generation.
208 display: Controls how the flag is displayed in automatic help
209 generation. Pass False to hide the flag, pass a string to
210 customize the label, and pass a FlagDisplay instance for full
211 customizability.
212 """
213 def __init__(self, name, parse_as=str, missing=None, multi='error',
214 char=None, doc=None, display=None):
215 self.name = flag_to_identifier(name)
216 self.doc = doc
217 self.parse_as = parse_as
218 self.missing = missing
219 if missing is ERROR and not callable(parse_as):
220 raise ValueError('cannot make an argument-less flag required.'
221 ' expected non-ERROR for missing, or a callable'
222 ' for parse_as, not: %r' % parse_as)
223 self.char = _validate_char(char) if char else None
225 if callable(multi):
226 self.multi = multi
227 elif multi in _MULTI_SHORTCUTS:
228 self.multi = _MULTI_SHORTCUTS[multi]
229 else:
230 raise ValueError('multi expected callable, bool, or one of %r, not: %r'
231 % (list(_MULTI_SHORTCUTS.keys()), multi))
233 self.set_display(display)
235 def set_display(self, display):
236 """Controls how the flag is displayed in automatic help
237 generation. Pass False to hide the flag, pass a string to
238 customize the label, and pass a FlagDisplay instance for full
239 customizability.
240 """
241 if display is None:
242 display = {}
243 elif isinstance(display, bool):
244 display = {'hidden': not display}
245 elif isinstance(display, str):
246 display = {'label': display}
247 if isinstance(display, dict):
248 display = FlagDisplay(self, **display)
249 if not isinstance(display, FlagDisplay):
250 raise TypeError('expected bool, text name, dict of display'
251 ' options, or FlagDisplay instance, not: %r'
252 % display)
253 self.display = display
255 def __repr__(self):
256 return format_nonexp_repr(self, ['name', 'parse_as'], ['missing', 'multi'],
257 opt_key=lambda v: v not in (None, _multi_error))
260class FlagDisplay(object):
261 """Provides individual overrides for most of a given flag's display
262 settings, as used by HelpFormatter instances attached to Parser
263 and Command objects. Pass an instance of this to
264 Flag.set_display() for full control of help output.
266 FlagDisplay instances are meant to be used 1:1 with Flag
267 instances, as they maintain a reference back to their associated
268 Flag. They are generally automatically created by a Flag
269 constructor, based on the "display" argument.
271 Args:
272 flag (Flag): The Flag instance to which this FlagDisplay applies.
273 label (str): The formatted version of the string used to
274 represent the flag in help and error messages. Defaults to
275 None, which allows the label to be autogenerated by the
276 HelpFormatter.
277 post_doc (str): An addendum string added to the Flag's own
278 doc. Defaults to a parenthetical describing whether the flag
279 takes an argument, and whether the argument is required.
280 full_doc (str): A string of the whole flag's doc, overriding
281 the doc + post_doc default.
282 value_name (str): For flags which take an argument, the string
283 to use as the placeholder of the flag argument in help and
284 error labels.
285 hidden (bool): Pass True to hide this flag in general help and
286 error messages. Defaults to False.
287 group: An integer or string indicating how this flag should be
288 grouped in help messages, improving readability. Integers are
289 unnamed groups, strings are for named groups. Defaults to 0.
290 sort_key: Flags are sorted in help output, pass an integer or
291 string to override the sort order.
293 """
294 # value_name -> arg_name?
295 def __init__(self, flag, **kw):
296 self.flag = flag
298 self.doc = flag.doc
299 if self.doc is None and callable(flag.parse_as):
300 _prep, desc = get_type_desc(flag.parse_as)
301 self.doc = 'Parsed with ' + desc
302 if _prep == 'as':
303 self.doc = desc
305 self.post_doc = kw.pop('post_doc', None)
306 self.full_doc = kw.pop('full_doc', None)
308 self.value_name = ''
309 if callable(flag.parse_as):
310 # TODO: use default when it's set and it's a basic renderable type
311 self.value_name = kw.pop('value_name', None) or self.flag.name.upper()
313 self.group = kw.pop('group', 0) # int or str
314 self._hide = kw.pop('hidden', False) # bool
315 self.label = kw.pop('label', None) # see hidden property below for more info
316 self.sort_key = kw.pop('sort_key', 0) # int or str
317 # TODO: sort_key is gonna need to be partitioned on type for py3
318 # TODO: maybe sort_key should be a counter so that flags sort
319 # in the order they are created
321 if kw:
322 raise TypeError('unexpected keyword arguments: %r' % kw.keys())
323 return
325 @property
326 def hidden(self):
327 return self._hide or self.label == ''
329 def __repr__(self):
330 return format_nonexp_repr(self, ['label', 'doc'], ['group', 'hidden'], opt_key=bool)
333class PosArgDisplay(object):
334 """Provides individual overrides for PosArgSpec display in automated
335 help formatting. Pass to a PosArgSpec constructor, which is in
336 turn passed to a Command/Parser.
338 Args:
339 spec (PosArgSpec): The associated PosArgSpec.
340 name (str): The string name of an individual positional
341 argument. Automatically pluralized in the label according to
342 PosArgSpec values. Defaults to 'arg'.
343 label (str): The full display label for positional arguments,
344 bypassing the automatic formatting of the *name* parameter.
345 doc (str): A summary description of the positional arguments.
346 post_doc (str): An informational addendum about the arguments,
347 often describes default behavior.
349 """
350 def __init__(self, **kw):
351 self.name = kw.pop('name', 'arg')
352 self.doc = kw.pop('doc', '')
353 self.post_doc = kw.pop('post_doc', None)
354 self._hide = kw.pop('hidden', False) # bool
355 self.label = kw.pop('label', None)
357 if kw:
358 raise TypeError('unexpected keyword arguments: %r' % kw.keys())
359 return
361 @property
362 def hidden(self):
363 return self._hide or self.label == ''
365 def __repr__(self):
366 return format_nonexp_repr(self, ['name', 'label'])
369class PosArgSpec(object):
370 """Passed to Command/Parser as posargs and post_posargs parameters to
371 configure the number and type of positional arguments.
373 Args:
374 parse_as (callable): A function to call on each of the passed
375 arguments. Also accepts special argument ERROR, which will raise
376 an exception if positional arguments are passed. Defaults to str.
377 min_count (int): A minimimum number of positional
378 arguments. Defaults to 0.
379 max_count (int): A maximum number of positional arguments. Also
380 accepts None, meaning no maximum. Defaults to None.
381 display: Pass a string to customize the name in help output, or
382 False to hide it completely. Also accepts a PosArgDisplay
383 instance, or a dict of the respective arguments.
384 provides (str): name of an argument to be passed to a receptive
385 handler function.
386 name (str): A shortcut to set *display* name and *provides*
387 count (int): A shortcut to set min_count and max_count to a single value
388 when an exact number of arguments should be specified.
390 PosArgSpec instances are stateless and safe to be used multiple
391 times around the application.
393 """
394 def __init__(self, parse_as=str, min_count=None, max_count=None, display=None, provides=None, **kwargs):
395 if not callable(parse_as) and parse_as is not ERROR:
396 raise TypeError('expected callable or ERROR for parse_as, not %r' % parse_as)
397 name = kwargs.pop('name', None)
398 count = kwargs.pop('count', None)
399 if kwargs:
400 raise TypeError('unexpected keyword arguments: %r' % list(kwargs.keys()))
401 self.parse_as = parse_as
403 # count convenience alias
404 min_count = count if min_count is None else min_count
405 max_count = count if max_count is None else max_count
407 self.min_count = int(min_count) if min_count else 0
408 self.max_count = int(max_count) if max_count is not None else None
410 if self.min_count < 0:
411 raise ValueError('expected min_count >= 0, not: %r' % self.min_count)
412 if self.max_count is not None and self.max_count <= 0:
413 raise ValueError('expected max_count > 0, not: %r' % self.max_count)
414 if self.max_count and self.min_count > self.max_count:
415 raise ValueError('expected min_count > max_count, not: %r > %r'
416 % (self.min_count, self.max_count))
418 provides = name if provides is None else provides
419 self.provides = provides
421 if display is None:
422 display = {}
423 elif isinstance(display, bool):
424 display = {'hidden': not display}
425 elif isinstance(display, str):
426 display = {'name': display}
427 if isinstance(display, dict):
428 display.setdefault('name', name)
429 display = PosArgDisplay(**display)
430 if not isinstance(display, PosArgDisplay):
431 raise TypeError('expected bool, text name, dict of display'
432 ' options, or PosArgDisplay instance, not: %r'
433 % display)
435 self.display = display
437 # TODO: default? type check that it's a sequence matching min/max reqs
439 def __repr__(self):
440 return format_nonexp_repr(self, ['parse_as', 'min_count', 'max_count', 'display'])
442 @property
443 def accepts_args(self):
444 """True if this PosArgSpec is configured to accept one or
445 more arguments.
446 """
447 return self.parse_as is not ERROR
449 def parse(self, posargs):
450 """Parse a list of strings as positional arguments.
452 Args:
453 posargs (list): List of strings, likely parsed by a Parser
454 instance from sys.argv.
456 Raises an ArgumentArityError if there are too many or too few
457 arguments.
459 Raises InvalidPositionalArgument if the argument doesn't match
460 the configured *parse_as*. See PosArgSpec for more info.
462 Returns a list of arguments, parsed with *parse_as*.
463 """
464 len_posargs = len(posargs)
465 if posargs and not self.accepts_args:
466 # TODO: check for likely subcommands
467 raise ArgumentArityError('unexpected positional arguments: %r' % posargs)
468 min_count, max_count = self.min_count, self.max_count
469 if min_count == max_count:
470 # min_count must be >0 because max_count cannot be 0
471 arg_range_text = '%s argument' % min_count
472 if min_count > 1:
473 arg_range_text += 's'
474 else:
475 if min_count == 0:
476 arg_range_text = 'up to %s argument' % max_count
477 arg_range_text += 's' if (max_count and max_count > 1) else ''
478 elif max_count is None:
479 arg_range_text = 'at least %s argument' % min_count
480 arg_range_text += 's' if min_count > 1 else ''
481 else:
482 arg_range_text = '%s - %s arguments' % (min_count, max_count)
484 if len_posargs < min_count:
485 raise ArgumentArityError('too few arguments, expected %s, got %s'
486 % (arg_range_text, len_posargs))
487 if max_count is not None and len_posargs > max_count:
488 raise ArgumentArityError('too many arguments, expected %s, got %s'
489 % (arg_range_text, len_posargs))
490 ret = []
491 for pa in posargs:
492 try:
493 val = self.parse_as(pa)
494 except Exception as exc:
495 raise InvalidPositionalArgument.from_parse(self, pa, exc)
496 else:
497 ret.append(val)
498 return ret
501FLAGFILE_ENABLED = Flag('--flagfile', parse_as=str, multi='extend', missing=None, display=False, doc='')
504def _ensure_posargspec(posargs, posargs_name):
505 if not posargs:
506 # take no posargs
507 posargs = PosArgSpec(parse_as=ERROR)
508 elif posargs is True:
509 # take any number of posargs
510 posargs = PosArgSpec()
511 elif isinstance(posargs, int):
512 # take an exact number of posargs
513 # (True and False are handled above, so only real nonzero ints get here)
514 posargs = PosArgSpec(min_count=posargs, max_count=posargs)
515 elif isinstance(posargs, str):
516 posargs = PosArgSpec(display=posargs, provides=posargs)
517 elif isinstance(posargs, dict):
518 posargs = PosArgSpec(**posargs)
519 elif callable(posargs):
520 # take any number of posargs of a given format
521 posargs = PosArgSpec(parse_as=posargs)
523 if not isinstance(posargs, PosArgSpec):
524 raise TypeError('expected %s as True, False, number of args, text name of args,'
525 ' dict of PosArgSpec options, or instance of PosArgSpec, not: %r'
526 % (posargs_name, posargs))
528 return posargs
531class Parser(object):
532 """The Parser lies at the center of face, primarily providing a
533 configurable validation logic on top of the conventional grammar
534 for CLI argument parsing.
536 Args:
537 name (str): A name used to identify this command. Important
538 when the command is embedded as a subcommand of another
539 command.
540 doc (str): An optional summary description of the command, used
541 to generate help and usage information.
542 flags (list): A list of Flag instances. Optional, as flags can
543 be added with :meth:`~Parser.add()`.
544 posargs (bool): Defaults to disabled, pass ``True`` to enable
545 the Parser to accept positional arguments. Pass a callable
546 to parse the positional arguments using that
547 function/type. Pass a :class:`PosArgSpec` for full
548 customizability.
549 post_posargs (bool): Same as *posargs*, but refers to the list
550 of arguments following the ``--`` conventional marker. See
551 ``git`` and ``tox`` for examples of commands using this
552 style of positional argument.
553 flagfile (bool): Defaults to enabled, pass ``False`` to disable
554 flagfile support. Pass a :class:`Flag` instance to use a
555 custom flag instead of ``--flagfile``. Read more about
556 Flagfiles below.
558 Once initialized, parsing is performed by calling
559 :meth:`Parser.parse()` with ``sys.argv`` or any other list of strings.
560 """
561 def __init__(self, name, doc=None, flags=None, posargs=None,
562 post_posargs=None, flagfile=True):
563 self.name = process_command_name(name)
564 self.doc = doc
565 flags = list(flags or [])
567 self.posargs = _ensure_posargspec(posargs, 'posargs')
568 self.post_posargs = _ensure_posargspec(post_posargs, 'post_posargs')
570 if flagfile is True:
571 self.flagfile_flag = FLAGFILE_ENABLED
572 elif isinstance(flagfile, Flag):
573 self.flagfile_flag = flagfile
574 elif not flagfile:
575 self.flagfile_flag = None
576 else:
577 raise TypeError('expected True, False, or Flag instance for'
578 ' flagfile, not: %r' % flagfile)
580 self.subprs_map = OrderedDict()
581 self._path_flag_map = OrderedDict()
582 self._path_flag_map[()] = OrderedDict()
584 for flag in flags:
585 self.add(flag)
586 if self.flagfile_flag:
587 self.add(self.flagfile_flag)
588 return
590 def get_flag_map(self, path, with_hidden=True):
591 flag_map = self._path_flag_map[path]
592 return OrderedDict([(k, f) for k, f in flag_map.items()
593 if with_hidden or not f.display.hidden])
595 def get_flags(self, path=(), with_hidden=True):
596 flag_map = self.get_flag_map(path=path, with_hidden=with_hidden)
598 return unique(flag_map.values())
600 def __repr__(self):
601 cn = self.__class__.__name__
602 return ('<%s name=%r subcmd_count=%r flag_count=%r posargs=%r>'
603 % (cn, self.name, len(self.subprs_map), len(self.get_flags()), self.posargs))
605 def _add_subparser(self, subprs):
606 """Process subcommand name, check for subcommand conflicts, check for
607 subcommand flag conflicts, then finally add subcommand.
609 To add a command under a different name, simply make a copy of
610 that parser or command with a different name.
611 """
612 if self.posargs.accepts_args:
613 raise ValueError('commands accepting positional arguments'
614 ' cannot take subcommands')
616 # validate that the subparser's name can be used as a subcommand
617 subprs_name = process_command_name(subprs.name)
619 # then, check for conflicts with existing subcommands and flags
620 for prs_path in self.subprs_map:
621 if prs_path[0] == subprs_name:
622 raise ValueError('conflicting subcommand name: %r' % subprs_name)
623 parent_flag_map = self._path_flag_map[()]
625 check_no_conflicts = lambda parent_flag_map, subcmd_path, subcmd_flags: True
626 for path, flags in subprs._path_flag_map.items():
627 if not check_no_conflicts(parent_flag_map, path, flags):
628 # TODO
629 raise ValueError('subcommand flags conflict with parent command: %r' % flags)
631 # with checks complete, add parser and all subparsers
632 self.subprs_map[(subprs_name,)] = subprs
633 for path, cur_subprs in subprs.subprs_map.items():
634 new_path = (subprs_name,) + path
635 self.subprs_map[new_path] = cur_subprs
637 # Flags inherit down (a parent's flags are usable by the child)
638 for path, flags in subprs._path_flag_map.items():
639 new_flags = parent_flag_map.copy()
640 new_flags.update(flags)
641 self._path_flag_map[(subprs_name,) + path] = new_flags
643 # If two flags have the same name, as long as the "parse_as"
644 # is the same, things should be ok. Need to watch for
645 # overlapping aliases, too. This may allow subcommands to
646 # further document help strings. Should the same be allowed
647 # for defaults?
649 def add(self, *a, **kw):
650 """Add a flag or subparser.
652 Unless the first argument is a Parser or Flag object, the
653 arguments are the same as the Flag constructor, and will be
654 used to create a new Flag instance to be added.
656 May raise ValueError if arguments are not recognized as
657 Parser, Flag, or Flag parameters. ValueError may also be
658 raised on duplicate definitions and other conflicts.
659 """
660 if isinstance(a[0], Parser):
661 subprs = a[0]
662 self._add_subparser(subprs)
663 return
665 if isinstance(a[0], Flag):
666 flag = a[0]
667 else:
668 try:
669 flag = Flag(*a, **kw)
670 except TypeError as te:
671 raise ValueError('expected Parser, Flag, or Flag parameters,'
672 ' not: %r, %r (got %r)' % (a, kw, te))
674 # first check there are no conflicts...
675 flag.name = flag.name
677 for subcmds, flag_map in self._path_flag_map.items():
678 conflict_flag = flag_map.get(flag.name) or (flag.char and flag_map.get(flag.char))
679 if conflict_flag is None:
680 continue
681 if flag.name in (conflict_flag.name, conflict_flag.char):
682 raise ValueError('pre-existing flag %r conflicts with name of new flag %r'
683 % (conflict_flag, flag.name))
684 if flag.char and flag.char in (conflict_flag.name, conflict_flag.char):
685 raise ValueError('pre-existing flag %r conflicts with short form for new flag %r'
686 % (conflict_flag, flag))
688 # ... then we add the flags
689 for flag_map in self._path_flag_map.values():
690 flag_map[flag.name] = flag
691 if flag.char:
692 flag_map[flag.char] = flag
693 return
695 def parse(self, argv):
696 """This method takes a list of strings and converts them into a
697 validated :class:`CommandParseResult` according to the flags,
698 subparsers, and other options configured.
700 Args:
701 argv (list): A required list of strings. Pass ``None`` to
702 use ``sys.argv``.
704 This method may raise ArgumentParseError (or one of its
705 subtypes) if the list of strings fails to parse.
707 .. note:: The *argv* parameter does not automatically default
708 to using ``sys.argv`` because it's best practice for
709 implementing codebases to perform that sort of
710 defaulting in their ``main()``, which should accept
711 an ``argv=None`` parameter. This simple step ensures
712 that the Python CLI application has some sort of
713 programmatic interface that doesn't require
714 subprocessing. See here for an example.
716 """
717 if argv is None:
718 argv = sys.argv
719 cpr = CommandParseResult(parser=self, argv=argv)
720 if not argv:
721 ape = ArgumentParseError('expected non-empty sequence of arguments, not: %r' % (argv,))
722 ape.prs_res = cpr
723 raise ape
724 for arg in argv:
725 if not isinstance(arg, (str, unicode)):
726 raise TypeError('parse expected all args as strings, not: %r (%s)' % (arg, type(arg).__name__))
727 '''
728 for subprs_path, subprs in self.subprs_map.items():
729 if len(subprs_path) == 1:
730 # _add_subparser takes care of recurring so we only
731 # need direct subparser descendants
732 self._add_subparser(subprs, overwrite=True)
733 '''
734 flag_map = None
735 # first snip off the first argument, the command itself
736 cmd_name, args = argv[0], list(argv)[1:]
737 cpr.name = cmd_name
739 # we record our progress as we parse to provide the most
740 # up-to-date info possible to the error and help handlers
742 try:
743 # then figure out the subcommand path
744 subcmds, args = self._parse_subcmds(args)
745 cpr.subcmds = tuple(subcmds)
747 prs = self.subprs_map[tuple(subcmds)] if subcmds else self
749 # then look up the subcommand's supported flags
750 # NOTE: get_flag_map() is used so that inheritors, like Command,
751 # can filter by actually-used arguments, not just
752 # available arguments.
753 cmd_flag_map = self.get_flag_map(path=tuple(subcmds))
755 # parse supported flags and validate their arguments
756 flag_map, flagfile_map, posargs = self._parse_flags(cmd_flag_map, args)
757 cpr.flags = OrderedDict(flag_map)
758 cpr.posargs = tuple(posargs)
760 # take care of dupes and check required flags
761 resolved_flag_map = self._resolve_flags(cmd_flag_map, flag_map, flagfile_map)
762 cpr.flags = OrderedDict(resolved_flag_map)
764 # separate out any trailing arguments from normal positional arguments
765 post_posargs = None # TODO: default to empty list?
766 parsed_post_posargs = None
767 if '--' in posargs:
768 posargs, post_posargs = split(posargs, '--', 1)
769 cpr.posargs, cpr.post_posargs = posargs, post_posargs
771 parsed_post_posargs = prs.post_posargs.parse(post_posargs)
772 cpr.post_posargs = tuple(parsed_post_posargs)
774 parsed_posargs = prs.posargs.parse(posargs)
775 cpr.posargs = tuple(parsed_posargs)
776 except ArgumentParseError as ape:
777 ape.prs_res = cpr
778 raise
780 return cpr
782 def _parse_subcmds(self, args):
783 """Expects arguments after the initial command (i.e., argv[1:])
785 Returns a tuple of (list_of_subcmds, remaining_args).
787 Raises on unknown subcommands."""
788 ret = []
790 for arg in args:
791 if arg.startswith('-'):
792 break # subcmd parsing complete
794 arg = _arg_to_subcmd(arg)
795 if tuple(ret + [arg]) not in self.subprs_map:
796 prs = self.subprs_map[tuple(ret)] if ret else self
797 if prs.posargs.parse_as is not ERROR or not prs.subprs_map:
798 # we actually have posargs from here
799 break
800 raise InvalidSubcommand.from_parse(prs, arg)
801 ret.append(arg)
802 return ret, args[len(ret):]
804 def _parse_single_flag(self, cmd_flag_map, args):
805 arg = args[0]
806 flag = cmd_flag_map.get(normalize_flag_name(arg))
807 if flag is None:
808 raise UnknownFlag.from_parse(cmd_flag_map, arg)
809 parse_as = flag.parse_as
810 if not callable(parse_as):
811 # e.g., True is effectively store_true, False is effectively store_false
812 return flag, parse_as, args[1:]
814 try:
815 arg_text = args[1]
816 except IndexError:
817 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg=None)
818 try:
819 arg_val = parse_as(arg_text)
820 except Exception as e:
821 raise InvalidFlagArgument.from_parse(cmd_flag_map, flag, arg_text, exc=e)
823 return flag, arg_val, args[2:]
825 def _parse_flags(self, cmd_flag_map, args):
826 """Expects arguments after the initial command and subcommands (i.e.,
827 the second item returned from _parse_subcmds)
829 Returns a tuple of (multidict of flag names to parsed and validated values, remaining_args).
831 Raises on unknown subcommands.
832 """
833 flag_value_map = OMD()
834 ff_path_res_map = OrderedDict()
835 ff_path_seen = set()
837 orig_args = args
838 while args:
839 arg = args[0]
840 if not arg or arg[0] != '-' or arg == '-' or arg == '--':
841 # posargs or post_posargs beginning ('-' is a conventional pos arg for stdin)
842 break
843 flag, value, args = self._parse_single_flag(cmd_flag_map, args)
844 flag_value_map.add(flag.name, value)
846 if flag is self.flagfile_flag:
847 self._parse_flagfile(cmd_flag_map, value, res_map=ff_path_res_map)
848 for path, ff_flag_value_map in ff_path_res_map.items():
849 if path in ff_path_seen:
850 continue
851 flag_value_map.update_extend(ff_flag_value_map)
852 ff_path_seen.add(path)
854 return flag_value_map, ff_path_res_map, args
856 def _parse_flagfile(self, cmd_flag_map, path_or_file, res_map=None):
857 ret = res_map if res_map is not None else OrderedDict()
858 if callable(getattr(path_or_file, 'read', None)):
859 # enable StringIO and custom flagfile opening
860 f_name = getattr(path_or_file, 'name', None)
861 path = os.path.abspath(f_name) if f_name else repr(path_or_file)
862 ff_text = path_or_file.read()
863 else:
864 path = os.path.abspath(path_or_file)
865 try:
866 with codecs.open(path_or_file, 'r', 'utf-8') as f:
867 ff_text = f.read()
868 except (UnicodeError, EnvironmentError) as ee:
869 raise ArgumentParseError('failed to load flagfile "%s", got: %r' % (path, ee))
870 if path in res_map:
871 # we've already seen this file
872 return res_map
873 ret[path] = cur_file_res = OMD()
874 lines = ff_text.splitlines()
875 for lineno, line in enumerate(lines, 1):
876 try:
877 args = shlex.split(line, comments=True)
878 if not args:
879 continue # comment or empty line
880 flag, value, leftover_args = self._parse_single_flag(cmd_flag_map, args)
882 if leftover_args:
883 raise ArgumentParseError('excessive flags or arguments for flag "%s",'
884 ' expected one flag per line' % flag.name)
886 cur_file_res.add(flag.name, value)
887 if flag is self.flagfile_flag:
888 self._parse_flagfile(cmd_flag_map, value, res_map=ret)
890 except FaceException as fe:
891 fe.args = (fe.args[0] + ' (on line %s of flagfile "%s")' % (lineno, path),)
892 raise
894 return ret
896 def _resolve_flags(self, cmd_flag_map, parsed_flag_map, flagfile_map=None):
897 ret = OrderedDict()
898 cfm, pfm = cmd_flag_map, parsed_flag_map
899 flagfile_map = flagfile_map or {}
901 # check requireds and set defaults and then...
902 missing_flags = []
903 for flag_name, flag in cfm.items():
904 if flag_name in pfm:
905 continue
906 if flag.missing is ERROR:
907 missing_flags.append(flag.name)
908 else:
909 pfm[flag_name] = flag.missing
910 if missing_flags:
911 raise MissingRequiredFlags.from_parse(cfm, pfm, missing_flags)
913 # ... resolve dupes
914 for flag_name in pfm:
915 flag = cfm[flag_name]
916 arg_val_list = pfm.getlist(flag_name)
917 try:
918 ret[flag_name] = flag.multi(flag, arg_val_list)
919 except FaceException as fe:
920 ff_paths = []
921 for ff_path, ff_value_map in flagfile_map.items():
922 if flag_name in ff_value_map:
923 ff_paths.append(ff_path)
924 if ff_paths:
925 ff_label = 'flagfiles' if len(ff_paths) > 1 else 'flagfile'
926 msg = ('\n\t(check %s with definitions for flag "%s": %s)'
927 % (ff_label, flag_name, ', '.join(ff_paths)))
928 fe.args = (fe.args[0] + msg,)
929 raise
930 return ret
933def parse_sv_line(line, sep=','):
934 """Parse a single line of values, separated by the delimiter
935 *sep*. Supports quoting.
937 """
938 # TODO: this doesn't support unicode, which is intended to be
939 # handled at the layer above.
940 from csv import reader, Dialect, QUOTE_MINIMAL
942 class _face_dialect(Dialect):
943 delimiter = sep
944 escapechar = '\\'
945 quotechar = '"'
946 doublequote = True
947 skipinitialspace = False
948 lineterminator = '\n'
949 quoting = QUOTE_MINIMAL
951 parsed = list(reader([line], dialect=_face_dialect))
952 return parsed[0]
955class ListParam(object):
956 """The ListParam takes an argument as a character-separated list, and
957 produces a Python list of parsed values. Basically, the argument
958 equivalent of CSV (Comma-Separated Values)::
960 --flag a1,b2,c3
962 By default, this yields a ``['a1', 'b2', 'c3']`` as the value for
963 ``flag``. The format is also similar to CSV in that it supports
964 quoting when values themselves contain the separator::
966 --flag 'a1,"b,2",c3'
968 Args:
969 parse_one_as (callable): Turns a single value's text into its
970 parsed value.
971 sep (str): A single-character string representing the list
972 value separator. Defaults to ``,``.
973 strip (bool): Whether or not each value in the list should have
974 whitespace stripped before being passed to
975 *parse_one_as*. Defaults to False.
977 .. note:: Aside from using ListParam, an alternative method for
978 accepting multiple arguments is to use the
979 ``multi=True`` on the :class:`Flag` constructor. The
980 approach tends to be more verbose and can be confusing
981 because arguments can get spread across the command
982 line.
984 """
985 def __init__(self, parse_one_as=str, sep=',', strip=False):
986 # TODO: min/max limits?
987 self.parse_one_as = parse_one_as
988 self.sep = sep
989 self.strip = strip
991 def parse(self, list_text):
992 "Parse a single string argument into a list of arguments."
993 split_vals = parse_sv_line(list_text, self.sep)
994 if self.strip:
995 split_vals = [v.strip() for v in split_vals]
996 return [self.parse_one_as(v) for v in split_vals]
998 __call__ = parse
1000 def __repr__(self):
1001 return format_exp_repr(self, ['parse_one_as'], ['sep', 'strip'])
1004class ChoicesParam(object):
1005 """Parses a single value, limited to a set of *choices*. The actual
1006 converter used to parse is inferred from *choices* by default, but
1007 an explicit one can be set *parse_as*.
1008 """
1009 def __init__(self, choices, parse_as=None):
1010 if not choices:
1011 raise ValueError('expected at least one choice, not: %r' % choices)
1012 try:
1013 self.choices = sorted(choices)
1014 except Exception:
1015 # in case choices aren't sortable
1016 self.choices = list(choices)
1017 if parse_as is None:
1018 parse_as = type(self.choices[0])
1019 # TODO: check for builtins, raise if not a supported type
1020 self.parse_as = parse_as
1022 def parse(self, text):
1023 choice = self.parse_as(text)
1024 if choice not in self.choices:
1025 raise ArgumentParseError('expected one of %r, not: %r' % (self.choices, text))
1026 return choice
1028 __call__ = parse
1030 def __repr__(self):
1031 return format_exp_repr(self, ['choices'], ['parse_as'])
1034class FilePathParam(object):
1035 """TODO
1037 ideas: exists, minimum permissions, can create, abspath, type=d/f
1038 (technically could also support socket, named pipe, and symlink)
1040 could do missing=TEMP, but that might be getting too fancy tbh.
1041 """
1043class FileValueParam(object):
1044 """
1045 TODO: file with a single value in it, like a pidfile
1046 or a password file mounted in. Read in and treated like it
1047 was on the argv.
1048 """