diff -r 44f455e6163d Lib/argparse.py --- a/Lib/argparse.py Thu Jun 27 12:23:29 2013 +0200 +++ b/Lib/argparse.py Thu Apr 24 22:15:49 2014 -0700 @@ -72,6 +72,7 @@ 'RawDescriptionHelpFormatter', 'RawTextHelpFormatter', 'MetavarTypeHelpFormatter', + 'ReGroupHelpFormatter', 'Namespace', 'Action', 'ONE_OR_MORE', @@ -311,21 +312,15 @@ # build full usage string format = self._format_actions_usage - action_usage = format(optionals + positionals, groups) - usage = ' '.join([s for s in [prog, action_usage] if s]) + action_parts = format(optionals + positionals, groups) + usage = ' '.join([s for s in [prog]+action_parts if s]) # wrap the usage parts if it's too long text_width = self._width - self._current_indent if len(prefix) + len(usage) > text_width: - # break usage into wrappable parts - part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' - opt_usage = format(optionals, groups) - pos_usage = format(positionals, groups) - opt_parts = _re.findall(part_regexp, opt_usage) - pos_parts = _re.findall(part_regexp, pos_usage) - assert ' '.join(opt_parts) == opt_usage - assert ' '.join(pos_parts) == pos_usage + opt_parts = format(optionals, groups) + pos_parts = format(positionals, groups) # helper for wrapping lines def get_lines(parts, indent, prefix=None): @@ -336,7 +331,7 @@ else: line_len = len(indent) - 1 for part in parts: - if line_len + 1 + len(part) > text_width: + if line and line_len + 1 + len(part) > text_width: lines.append(indent + ' '.join(line)) line = [] line_len = len(indent) - 1 @@ -377,61 +372,75 @@ return '%s%s\n\n' % (prefix, usage) def _format_actions_usage(self, actions, groups): - # find group indices and identify actions in groups - group_actions = set() - inserts = {} - for group in groups: - try: - start = actions.index(group._group_actions[0]) - except ValueError: - continue - else: - end = start + len(group._group_actions) - if actions[start:end] == group._group_actions: - for action in group._group_actions: - group_actions.add(action) - if not group.required: - if start in inserts: - inserts[start] += ' [' - else: - inserts[start] = '[' - inserts[end] = ']' - else: - if start in inserts: - inserts[start] += ' (' - else: - inserts[start] = '(' - inserts[end] = ')' - for i in range(start + 1, end): - inserts[i] = '|' - - # collect all actions format strings + # format the usage using the actions list. Where possible + # format the groups that include those actions + # The actions list has priority + # This is a new version that formats the groups directly without + # needing inserts, (most) cleanup, or parsing into parts parts = [] - for i, action in enumerate(actions): - - # suppressed arguments are marked with None - # remove | separators for suppressed arguments + i = 0 + # step through the actions list + while i1: + parts[-1] = ')' if group.required else ']' + else: + # nothing added + parts = [] + arg_parts = [''.join(parts)] + + def cleanup(text): + # remove unnecessary () + text = _re.sub(r'^\(([^|]*)\)$', r'\1', text) + return text + arg_parts = [cleanup(t) for t in arg_parts] + return arg_parts + + def _format_just_actions_usage(self, actions): + # actions, without any group markings + parts = [] + for action in actions: if action.help is SUPPRESS: - parts.append(None) - if inserts.get(i) == '|': - inserts.pop(i) - elif inserts.get(i + 1) == '|': - inserts.pop(i + 1) - - # produce all arg strings + pass elif not action.option_strings: default = self._get_default_metavar_for_positional(action) part = self._format_args(action, default) - - # if it's in a group, strip the outer [] - if action in group_actions: - if part[0] == '[' and part[-1] == ']': - part = part[1:-1] - - # add the action string to the list parts.append(part) - - # produce the first way to invoke the option in brackets else: option_string = action.option_strings[0] @@ -447,31 +456,11 @@ args_string = self._format_args(action, default) part = '%s %s' % (option_string, args_string) - # make it look optional if it's not required or in a group - if not action.required and action not in group_actions: + # make it look optional if it's not required + if not action.required: part = '[%s]' % part - - # add the action string to the list parts.append(part) - - # insert things at the necessary indices - for i in sorted(inserts, reverse=True): - parts[i:i] = [inserts[i]] - - # join all the action items with spaces - text = ' '.join([item for item in parts if item is not None]) - - # clean up separators for mutually exclusive groups - open = r'[\[(]' - close = r'[\])]' - text = _re.sub(r'(%s) ' % open, r'\1', text) - text = _re.sub(r' (%s)' % close, r'\1', text) - text = _re.sub(r'%s *%s' % (open, close), r'', text) - text = _re.sub(r'\(([^|]*)\)', r'\1', text) - text = text.strip() - - # return the text - return text + return parts def _format_text(self, text): if '%(prog)' in text: @@ -681,7 +670,55 @@ def _get_default_metavar_for_positional(self, action): return action.type.__name__ - +class ReGroupHelpFormatter(HelpFormatter): + """Help message formatter that handles regrouping of positional args + e.g. '[arg1] [arg2]' => '[arg1 [arg2]]' + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + + Thie regroups optional positionals + """ + + def _format_actions_usage(self, actions, groups): + self._deduce_prefixchars(actions) + parts = super(ReGroupHelpFormatter, self)._format_actions_usage(actions, groups) + #print('parts',parts) + parts = self._positionals_regroup(parts) + #print(parts) + return parts + + def _deduce_prefixchars(self, actions): + optionals = [action for action in actions if action.option_strings] + chars = {str[0] for action in optionals for str in action.option_strings} + self.prefix_chars = chars # make available to regroup fn + + def _positionals_regroup(self, parts): + # change ['[arg1]','[arg2]'] to ['[arg1 [arg2]]'] + # apply to a list of formatted arguments + # don't apply to optionals (with prefix chars) or groups + chars = getattr(self, 'prefix_chars',set('-')) + R = _re.compile(r'\] \[(.*)\]') + result = [] + text = None + while parts: + part = parts.pop() + if part: + if part[0]=='[' and part[1] not in chars and '|' not in part: + if text: + text = ' '.join([part, text]) + else: + text = part + if R.search(text): + text = R.sub(' [\g<1>]]',text) + else: + if text: + result.insert(0,text) + text = None + result.insert(0,part) + if text: + result.insert(0,text) + return result # ===================== # Options and Arguments