generator: Reimplement wrapping of 'description'

The current implementation of wrapping for the Policy.description field
leaves a lot to be desired. It takes each line in the original
description independently and wraps that, ignoring the fact that the
line may be part of a paragraph, or could be a literal that shouldn't be
wrapped. As an example, imagine that wrapping occurred at 40
characters instead of 70. In this case, the below:

    A sample lines with more than forty characters
    which continues down onto the next line.

would become:

    A sample lines with more than forty
    characters
    which continues down onto the next line.

when clearly, what we want is something like this:

    A sample lines with more than forty
    characters which continues down onto the
    next line.

This is resolved.

In addition, it should be possible to include basic literal blocks and
those should be preserved. For example:

    Here's a sample literal block

        This is a really long literal block but it should not wrap

should be output in the exact same way. This is also resolved.

Note that we're not accounting for things like bullet points. Doing so
would bring us close to rST parser implementation territory, and we
don't want to go there. We can revisit this if someone turns out to want
this feature.

Change-Id: I3ea2aac73e3c0a4f77f3f4097540de01264cd618
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane 2017-07-20 14:30:02 +01:00
parent 9fbbda81f1
commit bc9a91d8b7
2 changed files with 113 additions and 27 deletions

View File

@ -13,6 +13,7 @@
import logging
import sys
import textwrap
import warnings
from oslo_config import cfg
import stevedore
@ -89,19 +90,45 @@ def _format_help_text(description):
if not description:
return '#'
lines = description.splitlines()
formatted_lines = []
# wrap each line to support multi line descriptions
for line in lines:
if not line:
paragraph = []
def _wrap_paragraph(lines):
return textwrap.wrap(' '.join(lines), 70, initial_indent='# ',
subsequent_indent='# ')
for line in description.strip().splitlines():
if not line.strip():
# empty line -> line break, so dump anything we have
formatted_lines.extend(_wrap_paragraph(paragraph))
formatted_lines.append('#')
paragraph = []
elif len(line) == len(line.lstrip()):
# no leading whitespace = paragraph, which should be wrapped
paragraph.append(line.rstrip())
else:
formatted_lines.append(textwrap.fill(line, 70,
initial_indent='# ',
subsequent_indent='# ',
break_long_words=False,
replace_whitespace=False))
return "\n".join(formatted_lines)
# leading whitespace - literal block, which should not be wrapping
if paragraph:
# ...however, literal blocks need a new line before them to
# delineate things
# TODO(stephenfin): Raise an exception here and stop doing
# anything else in oslo.policy 2.0
warnings.warn(
'Invalid policy description: literal blocks must be '
'preceded by a new line. This will raise an exception in '
'a future version of oslo.policy:\n%s' % description,
FutureWarning)
formatted_lines.extend(_wrap_paragraph(paragraph))
formatted_lines.append('#')
paragraph = []
formatted_lines.append('# %s' % line.rstrip())
if paragraph:
# dump anything we might still have in the buffer
formatted_lines.extend(_wrap_paragraph(paragraph))
return '\n'.join(formatted_lines)
def _format_rule_default_yaml(default, include_help=True):

View File

@ -11,6 +11,7 @@
import operator
import sys
import warnings
import fixtures
import mock
@ -227,10 +228,9 @@ class GenerateSampleYAMLTestCase(base.PolicyBaseTestCase):
expected = '''# Create a bar.
#"foo:create_bar": "role:fizz"
# DEPRECATED
# "foo:post_bar":"role:fizz" has been deprecated since N in favor of
# "foo:create_bar":"role:fizz".
# foo:post_bar is being removed in favor of foo:create_bar
# DEPRECATED "foo:post_bar":"role:fizz" has been deprecated since N in
# favor of "foo:create_bar":"role:fizz". foo:post_bar is being removed
# in favor of foo:create_bar
"foo:post_bar": "rule:foo:create_bar"
'''
stdout = self._capture_stdout()
@ -244,26 +244,15 @@ class GenerateSampleYAMLTestCase(base.PolicyBaseTestCase):
)
self.assertEqual(expected, stdout.getvalue())
def test_empty_line_formatting(self):
def _test_formatting(self, description, expected):
rule = [policy.RuleDefault('admin', 'is_admin:True',
description='Check Summary \n'
'\n'
'This is a description to '
'check that empty line has '
'no white spaces.')]
description=description)]
ext = stevedore.extension.Extension(name='check_rule',
entry_point=None,
plugin=None, obj=rule)
test_mgr = stevedore.named.NamedExtensionManager.make_test_instance(
extensions=[ext], namespace=['check_rule'])
# no whitespace on empty line
expected = '''# Check Summary
#
# This is a description to check that empty line has no white spaces.
#"admin": "is_admin:True"
'''
output_file = self.get_config_file_fullname('policy.yaml')
with mock.patch('stevedore.named.NamedExtensionManager',
return_value=test_mgr) as mock_ext_mgr:
@ -278,6 +267,76 @@ class GenerateSampleYAMLTestCase(base.PolicyBaseTestCase):
self.assertEqual(expected, written_policy)
def test_empty_line_formatting(self):
description = ('Check Summary \n'
'\n'
'This is a description to '
'check that empty line has '
'no white spaces.')
expected = """# Check Summary
#
# This is a description to check that empty line has no white spaces.
#"admin": "is_admin:True"
"""
self._test_formatting(description, expected)
def test_paragraph_formatting(self):
description = """
Here's a neat description with a paragraph. We want to make sure that it wraps
properly.
"""
expected = """# Here's a neat description with a paragraph. We want \
to make sure
# that it wraps properly.
#"admin": "is_admin:True"
"""
self._test_formatting(description, expected)
def test_literal_block_formatting(self):
description = """Here's another description.
This one has a literal block.
These lines should be kept apart.
They should not be wrapped, even though they may be longer than 70 chars
"""
expected = """# Here's another description.
#
# This one has a literal block.
# These lines should be kept apart.
# They should not be wrapped, even though they may be longer than 70 chars
#"admin": "is_admin:True"
"""
self._test_formatting(description, expected)
def test_invalid_formatting(self):
description = """Here's a broken description.
We have some text...
Followed by a literal block without any spaces.
We don't support definition lists, so this is just wrong!
"""
expected = """# Here's a broken description.
#
# We have some text...
#
# Followed by a literal block without any spaces.
# We don't support definition lists, so this is just wrong!
#"admin": "is_admin:True"
"""
with warnings.catch_warnings(record=True) as warns:
self._test_formatting(description, expected)
self.assertEqual(1, len(warns))
self.assertTrue(issubclass(warns[-1].category, FutureWarning))
self.assertIn('Invalid policy description', str(warns[-1].message))
class GenerateSampleJSONTestCase(base.PolicyBaseTestCase):
def setUp(self):