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"""