diff -r 44f455e6163d Doc/library/argparse.rst --- a/Doc/library/argparse.rst Thu Jun 27 12:23:29 2013 +0200 +++ b/Doc/library/argparse.rst Thu Jul 31 18:21:20 2014 -0700 @@ -1867,6 +1867,112 @@ .. _upgrading-optparse-code: + +WhitespaceStyle +^^^^^^^^^^^^^^^ + +An alternative to changing the formatter_class_ is to +create a text block as :class:`str` subclass which implements the +desired formatting behavior (wrapping and whitespace compression). +This is akin to marking HTML text with a `
` tag, or the
+CSS white-space: option.
+
+Currently there are five classes, corresponding to the CSS options.
+
+.. class:: Normal
+           Pre
+           NoWrap
+           PreLine
+           PreWrap
+
+:class:`Normal` implements the default wrapping behavior,
+as performed by the :class:`HelpFormatter`, including white-space
+compression.
+
+:class:`Pre` is used for preformatted text, preserving all white space
+and new lines.  If both the description_ and epilog_ texts are of type
+:class:`Pre`, the help_ display will be same as that produced by the
+:class:`RawDescriptionHelpFormatter`.  If the argument help_ text is also
+of type :class:`Pre` the display will match that produced by
+:class:`RawTextHelpFormatter`.
+
+For convenience ``textwrap.dedent()`` is available as a method for each of
+these Style classes::
+
+   >>> parser = argparse.ArgumentParser(
+   ...     prog='PROG',
+   ...     description=argparse.Pre('''\
+   ...         Please do not mess up this text!
+   ...         --------------------------------
+   ...             I have indented it
+   ...             exactly the way
+   ...             I want it
+   ...         ''').dedent())
+   >>> parser.print_help()
+   usage: PROG [-h]
+
+   Please do not mess up this text!
+   --------------------------------
+      I have indented it
+      exactly the way
+      I want it
+
+   optional arguments:
+    -h, --help  show this help message and exit
+
+
+With these classes it is possible to mix the formatting style of the
+different parts of the help_ text.  For example, the description_ could be
+a :class:`PreLine`, with multiple individually wrapped paragraphs.
+The epilog_ could be :class:`Pre` with examples, while the help_ text
+could be left unmarked (or of :class:`Normal`)::
+
+
+   >>> parser = argparse.ArgumentParser(
+   ...     prog='PROG',
+   ...     description=argparse.PreLine('''\
+   ...         This is a description composed of several paragraphs that are wrapped individually.
+   ...
+   ...         %(prog)s arguments are as follows.
+   ...         ''').dedent(),
+   ...     epilog=argparse.Pre('''\
+   ...         %(prog)s example:
+   ...           %(prog)s --other 1
+   ...         ''').dedent())
+   >>> parser.add_argument('--text', help='a normal help line')
+   >>> parser.add_argument('--other', default='test',
+   ...     help=argparse.Pre("help line with hanging\n\t(default:%(default)r)"))
+   >>> parser.print_help()
+   usage: PROG [-h] [--text TEXT] [--other OTHER]
+
+   This is a description composed of several paragraphs that are wrapped
+   individually.
+
+   PROG arguments are as follows.
+
+   optional arguments:
+     -h, --help     show this help message and exit
+     --text TEXT    a normal help line
+     --other OTHER  help line with hanging
+                           (default:'test')
+
+   PROG example:
+     PROG --other 1
+
+
+
+
+.. class:: WhitespaceStyle
+           WSList
+
+:class:`WhitespaceStyle` is the parent class for these Style classes,
+which may be used to define custom formatting actions.  :class:`WSList` is a subclass of :class:`list`, with strings and Style
+class strings.  It may be used to compose a text that uses several types
+of formatting.
+
+For now these Style classes only work with the default :class:`HelpFormatter`.
+
+
 Upgrading optparse code
 -----------------------
 
diff -r 44f455e6163d Lib/argparse.py
--- a/Lib/argparse.py	Thu Jun 27 12:23:29 2013 +0200
+++ b/Lib/argparse.py	Thu Jul 31 18:21:20 2014 -0700
@@ -80,6 +80,14 @@
     'REMAINDER',
     'SUPPRESS',
     'ZERO_OR_MORE',
+    'WhitespaceStyle',
+    'Normal',
+    'Pre',
+    'NoWrap',
+    'PreWrap',
+    'PreLine',
+    'WSList',
+    'Py3FormatHelpFormatter',
 ]
 
 
@@ -137,6 +145,194 @@
     return getattr(namespace, name)
 
 
+# =============================
+# CSS white-space like formmating
+# =============================
+
+class WhitespaceStyle(str):
+    """ parent for classes that implement wrapping (or not) in the style
+    of CSS white-space:
+    """
+    def dedent(self):
+        return self.copy_class(_textwrap.dedent(self))
+
+    def copy_class(self, text):
+        """preserve self's class in the returned text (or list of lines)
+        class information like this is readily lost in str operations like join
+        """
+        Fn = type(self)
+        if isinstance(text, str):
+            return Fn(text)
+        else:
+            # this may not be of any value since a list like this normally joined
+            return [Fn(line) for line in text]
+
+    def _str_format(self, adict):
+        # apply % formatting
+        text = self % adict
+        return self.copy_class(text)
+
+    def format(self, *args, **kwargs):
+        # apply Py3 style format()
+        text = super(WhitespaceStyle,self).format(*args, **kwargs)
+        return self.copy_class(text)
+
+    def block(self, keepblank=False):
+        # divide text in paragraphs - block of text lines returned
+        # as one line, defined by blank line
+        # adapted from issue12806 paragraphFormatter
+        # no special handling for indented lines
+        # may keep a blank line (' ') between blocks
+        text = self
+
+        def blocker (text):
+            block = []
+            for line in text.splitlines():
+                isblank = _re.match(r'\s*$', line)
+                if isblank:
+                    if block:
+                        yield ' '.join(block)
+                        block = []
+                    if keepblank:
+                        yield ' '
+                else:
+                    block.append(line)
+            if block:
+                yield (' '.join(block))
+
+        lines = list(blocker(text))
+        lines = '\n'.join(lines)
+        return self.copy_class(lines)
+
+
+class WSList(list):
+    # a list of WhitespaceStyle objects
+    # meant to be called like a WhitespaceStyle object, applying the
+    # method to each of its items
+    # may need to extend to handle str in Normal()
+    # e.g. iterator that converts str to Normal
+
+    def __contains__(self, key):
+        # e.g. for '%' in self
+        return any(l.__contains__(key) for l in self)
+
+    def _split_lines(self, width):
+        lines = []
+        for p in self:
+            lines.extend(p._split_lines(width))
+        return WSList(lines)
+
+    def _fill_text(self, width, indent):
+        lines = []
+        for p in self:
+            lines.append(p._fill_text(width, indent))
+        return '\n'.join(lines)
+
+    def _str_format(self, adict):
+        return WSList([x._str_format(adict) for x in self])
+
+    def format(self, *args, **kwargs):
+        return WSList([x.format(*args, **kwargs) for x in self])
+
+
+class Normal(WhitespaceStyle):
+    """Sequences of whitespace are collapsed. Newline characters in the
+    source are handled as other whitespace. Breaks lines as necessary to fill line boxes.
+    Acts same as the default base str class; convenience class
+    """
+    _whitespace_matcher = _re.compile(r'\s+')
+    def _split_lines(self, width):
+        text = self
+        text = self._whitespace_matcher.sub(' ', text).strip()
+        lines = _textwrap.wrap(text, width)
+        return self.copy_class(lines)
+
+    def _fill_text(self, width, indent):
+        text = self
+        text = self._whitespace_matcher.sub(' ', text).strip()
+        text = _textwrap.fill(text, width, initial_indent=indent,
+                                           subsequent_indent=indent)
+        return self.copy_class(text)
+
+class Pre(WhitespaceStyle):
+    """Sequences of whitespace are preserved, lines are only broken
+    at newline characters in the source and at 
elements. + Acts same as the Raw...HelpFormatter classes + """ + def _split_lines(self, width): + return self.copy_class(self.splitlines()) + + def _fill_text(self, width, indent): + text = ''.join(indent + line for line in self.splitlines(keepends=True)) + return self.copy_class(text) + +class NoWrap(WhitespaceStyle): + """Collapses whitespace as for normal, but suppresses line breaks + (text wrapping) within text. + """ + _whitespace_matcher = _re.compile(r'[ \t\r\f\v]+') # whitespace excelude \n + def _split_lines(self, width): + text = self.strip().splitlines() + lines = [] + for line in text: + line = self._whitespace_matcher.sub(' ', line).strip() + lines.append(line) + return self.copy_class(lines) + + def _fill_text(self, width, indent): + text = self.strip().splitlines() + lines = [] + for line in text: + line = self._whitespace_matcher.sub(' ', line).strip() + lines.append(indent + line) + return self.copy_class('\n'.join(lines)) + +class PreWrap(WhitespaceStyle): + """Sequences of whitespace are preserved. Lines are broken at newline characters, + and as necessary to fill line boxes.""" + def _split_lines(self, width): + text = self.splitlines() + lines = [] + for line in text: + lines.extend(_textwrap.wrap(line, width)) + return self.copy_class(lines) + + def _fill_text(self, width, indent): + text = self.splitlines() + lines = [] + for line in text: + newline = _textwrap.fill(line, width, initial_indent=indent, + subsequent_indent=indent) + lines.append(newline) + text = "\n".join(lines) + return self.copy_class(text) + +class PreLine(WhitespaceStyle): + """Sequences of whitespace are collapsed. Lines are broken at newline + characters, and as necessary to fill line boxes.""" + _whitespace_matcher = _re.compile(r'[ \t\r\f\v]+') # whitespace excelude \n + def _split_lines(self, width): + text = self.splitlines() + lines = [] + for line in text: + line = self._whitespace_matcher.sub(' ', line).strip() + lines.extend(_textwrap.wrap(line, width)) + return self.copy_class(lines) + + def _fill_text(self, width, indent, subsequent_indent=None): + if subsequent_indent is None: + subsequent_indent = indent + text = self.splitlines() + lines = [] + for line in text: + line = self._whitespace_matcher.sub(' ', line).strip() + newline = _textwrap.fill(line, width, initial_indent=indent, + subsequent_indent=subsequent_indent) + lines.append(newline) + text = "\n".join(lines) + return self.copy_class(text) + + # =============== # Formatting Help # =============== @@ -290,7 +486,7 @@ # if usage is specified, use that if usage is not None: - usage = usage % dict(prog=self._prog) + usage = self._str_format(usage, dict(prog=self._prog)) # if no optionals or positionals are available, usage is just prog elif usage is None and not actions: @@ -474,8 +670,10 @@ return text def _format_text(self, text): - if '%(prog)' in text: - text = text % dict(prog=self._prog) + #if '%(prog)' in text: + # text = self._str_format(text, dict(prog=self._prog)) + # doesn't need to be conditional, does it? + text = self._str_format(text, dict(prog=self._prog)) text_width = self._width - self._current_indent indent = ' ' * self._current_indent return self._fill_text(text, text_width, indent) + '\n\n' @@ -597,7 +795,7 @@ if params.get('choices') is not None: choices_str = ', '.join([str(c) for c in params['choices']]) params['choices'] = choices_str - return self._get_help_string(action) % params + return self._str_format(self._get_help_string(action), params) def _iter_indented_subactions(self, action): try: @@ -610,13 +808,30 @@ self._dedent() def _split_lines(self, text, width): - text = self._whitespace_matcher.sub(' ', text).strip() - return _textwrap.wrap(text, width) + try: + return text._split_lines(width) + except AttributeError: + return Normal(text)._split_lines(width) def _fill_text(self, text, width, indent): - text = self._whitespace_matcher.sub(' ', text).strip() - return _textwrap.fill(text, width, initial_indent=indent, - subsequent_indent=indent) + try: + return text._fill_text(width, indent) + except AttributeError: + return Normal(text)._fill_text(width, indent) + + def _str_format(self, text, adict): + # apply % formatting + if isinstance(text, [WhitespaceStyle, WSList]): + return text._str_format(adict) + else: + return text % adict + + def _str_format(self, text, adict): + # apply % formatting; alt logic + try: + return text._str_format(adict) + except AttributeError: + return Normal(text)._str_format(adict) def _get_help_string(self, action): return action.help @@ -683,6 +898,26 @@ +class Py3FormatHelpFormatter(HelpFormatter): + """Help message formatter which accepts the Py3 string format function. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def _str_format(self, text, adict): + # handle both % and format styles + if '%(' in text: + return super(Py3FormatHelpFormatter, self)._str_format(text, adict) + else: + try: + # protect against % style text that may have a string + # that looks like a new style (e.g. {test}) + return text.format(**adict) + except KeyError: + pass + return text + # ===================== # Options and Arguments # ===================== diff -r 44f455e6163d Lib/test/test_argparse.py --- a/Lib/test/test_argparse.py Thu Jun 27 12:23:29 2013 +0200 +++ b/Lib/test/test_argparse.py Thu Jul 31 18:21:20 2014 -0700 @@ -3957,6 +3957,102 @@ version = '' +Pre = argparse.Pre +class TestHelpPreFormattedText(HelpTestCase): + """Test the Pre alternative to RawTextHelpFormatter""" + + parser_signature = Sig( + prog='PROG', + description=Pre('Keep the formatting\n' + ' exactly as it is written\n' + '\n' + 'here\n')) + + argument_signatures = [ + Sig('--foo', help=Pre(' foo help should also\n' + 'appear as given here')), + Sig('spam', help='spam help'), + ] + argument_group_signatures = [ + (Sig('title', description=Pre(' This text\n' + ' should be indented\n' + ' exactly like it is here\n')), + [Sig('--bar', help='bar help')]), + ] + usage = '''\ + usage: PROG [-h] [--foo FOO] [--bar BAR] spam + ''' + help = usage + '''\ + + Keep the formatting + exactly as it is written + + here + + positional arguments: + spam spam help + + optional arguments: + -h, --help show this help message and exit + --foo FOO foo help should also + appear as given here + + title: + This text + should be indented + exactly like it is here + + --bar BAR bar help + ''' + version = '' + +class TestHelpPreFormattedDescription(HelpTestCase): + """Test the Pre alternative to RawTextHelpFormatter""" + + parser_signature = Sig( + prog='PROG', + description=Pre('Keep the formatting\n' + ' exactly as it is written\n' + '\n' + 'here\n')) + + argument_signatures = [ + Sig('--foo', help=' foo help should not\n' + ' retain this odd formatting'), + Sig('spam', help='spam help'), + ] + argument_group_signatures = [ + (Sig('title', description=Pre(' This text\n' + ' should be indented\n' + ' exactly like it is here\n')), + [Sig('--bar', help='bar help')]), + ] + usage = '''\ + usage: PROG [-h] [--foo FOO] [--bar BAR] spam + ''' + help = usage + '''\ + + Keep the formatting + exactly as it is written + + here + + positional arguments: + spam spam help + + optional arguments: + -h, --help show this help message and exit + --foo FOO foo help should not retain this odd formatting + + title: + This text + should be indented + exactly like it is here + + --bar BAR bar help + ''' + version = '' + class TestHelpArgumentDefaults(HelpTestCase): """Test the ArgumentDefaultsHelpFormatter"""