blob: bf3ebaddfd9b244acfa319c1bcf8e8896ff34704 [file] [log] [blame]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001import sys
2import argparse
3from collections import defaultdict, OrderedDict
4
5class ArgumentUsageError(Exception):
6 """Exception class you can raise (and catch) in order to show the help"""
7 def __init__(self, message, subcommand=None):
8 self.message = message
9 self.subcommand = subcommand
10
11class ArgumentParser(argparse.ArgumentParser):
12 """Our own version of argparse's ArgumentParser"""
13 def __init__(self, *args, **kwargs):
14 kwargs.setdefault('formatter_class', OeHelpFormatter)
15 self._subparser_groups = OrderedDict()
16 super(ArgumentParser, self).__init__(*args, **kwargs)
17
18 def error(self, message):
19 sys.stderr.write('ERROR: %s\n' % message)
20 self.print_help()
21 sys.exit(2)
22
23 def error_subcommand(self, message, subcommand):
24 if subcommand:
25 for action in self._actions:
26 if isinstance(action, argparse._SubParsersAction):
27 for choice, subparser in action.choices.items():
28 if choice == subcommand:
29 subparser.error(message)
30 return
31 self.error(message)
32
33 def add_subparsers(self, *args, **kwargs):
34 ret = super(ArgumentParser, self).add_subparsers(*args, **kwargs)
35 # Need a way of accessing the parent parser
36 ret._parent_parser = self
37 # Ensure our class gets instantiated
38 ret._parser_class = ArgumentSubParser
39 # Hacky way of adding a method to the subparsers object
40 ret.add_subparser_group = self.add_subparser_group
41 return ret
42
43 def add_subparser_group(self, groupname, groupdesc, order=0):
44 self._subparser_groups[groupname] = (groupdesc, order)
45
46
47class ArgumentSubParser(ArgumentParser):
48 def __init__(self, *args, **kwargs):
49 if 'group' in kwargs:
50 self._group = kwargs.pop('group')
51 if 'order' in kwargs:
52 self._order = kwargs.pop('order')
53 super(ArgumentSubParser, self).__init__(*args, **kwargs)
54 for agroup in self._action_groups:
55 if agroup.title == 'optional arguments':
56 agroup.title = 'options'
57 break
58
59 def parse_known_args(self, args=None, namespace=None):
60 # This works around argparse not handling optional positional arguments being
61 # intermixed with other options. A pretty horrible hack, but we're not left
62 # with much choice given that the bug in argparse exists and it's difficult
63 # to subclass.
64 # Borrowed from http://stackoverflow.com/questions/20165843/argparse-how-to-handle-variable-number-of-arguments-nargs
65 # with an extra workaround (in format_help() below) for the positional
66 # arguments disappearing from the --help output, as well as structural tweaks.
67 # Originally simplified from http://bugs.python.org/file30204/test_intermixed.py
68 positionals = self._get_positional_actions()
69 for action in positionals:
70 # deactivate positionals
71 action.save_nargs = action.nargs
72 action.nargs = 0
73
74 namespace, remaining_args = super(ArgumentSubParser, self).parse_known_args(args, namespace)
75 for action in positionals:
76 # remove the empty positional values from namespace
77 if hasattr(namespace, action.dest):
78 delattr(namespace, action.dest)
79 for action in positionals:
80 action.nargs = action.save_nargs
81 # parse positionals
82 namespace, extras = super(ArgumentSubParser, self).parse_known_args(remaining_args, namespace)
83 return namespace, extras
84
85 def format_help(self):
86 # Quick, restore the positionals!
87 positionals = self._get_positional_actions()
88 for action in positionals:
89 if hasattr(action, 'save_nargs'):
90 action.nargs = action.save_nargs
91 return super(ArgumentParser, self).format_help()
92
93
94class OeHelpFormatter(argparse.HelpFormatter):
95 def _format_action(self, action):
96 if hasattr(action, '_get_subactions'):
97 # subcommands list
98 groupmap = defaultdict(list)
99 ordermap = {}
100 subparser_groups = action._parent_parser._subparser_groups
101 groups = sorted(subparser_groups.keys(), key=lambda item: subparser_groups[item][1], reverse=True)
102 for subaction in self._iter_indented_subactions(action):
103 parser = action._name_parser_map[subaction.dest]
104 group = getattr(parser, '_group', None)
105 groupmap[group].append(subaction)
106 if group not in groups:
107 groups.append(group)
108 order = getattr(parser, '_order', 0)
109 ordermap[subaction.dest] = order
110
111 lines = []
112 if len(groupmap) > 1:
113 groupindent = ' '
114 else:
115 groupindent = ''
116 for group in groups:
117 subactions = groupmap[group]
118 if not subactions:
119 continue
120 if groupindent:
121 if not group:
122 group = 'other'
123 groupdesc = subparser_groups.get(group, (group, 0))[0]
124 lines.append(' %s:' % groupdesc)
125 for subaction in sorted(subactions, key=lambda item: ordermap[item.dest], reverse=True):
126 lines.append('%s%s' % (groupindent, self._format_action(subaction).rstrip()))
127 return '\n'.join(lines)
128 else:
129 return super(OeHelpFormatter, self)._format_action(action)