diff options
Diffstat (limited to 'src/mailman/utilities')
| -rw-r--r-- | src/mailman/utilities/i18n.py | 6 | ||||
| -rw-r--r-- | src/mailman/utilities/string.py | 93 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_templates.py | 10 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_wrap.py | 151 |
4 files changed, 246 insertions, 14 deletions
diff --git a/src/mailman/utilities/i18n.py b/src/mailman/utilities/i18n.py index afaf26bc0..8e769329c 100644 --- a/src/mailman/utilities/i18n.py +++ b/src/mailman/utilities/i18n.py @@ -31,15 +31,11 @@ import os import errno from itertools import product -from zope.component import getUtility from mailman.config import config from mailman.core.constants import system_preferences from mailman.core.i18n import _ -from mailman.interfaces.languages import ILanguageManager -from mailman.utilities.string import expand - -from mailman.Utils import wrap as wrap_text +from mailman.utilities.string import expand, wrap as wrap_text diff --git a/src/mailman/utilities/string.py b/src/mailman/utilities/string.py index 3eda0dc39..9054ed076 100644 --- a/src/mailman/utilities/string.py +++ b/src/mailman/utilities/string.py @@ -25,6 +25,7 @@ __all__ = [ 'oneline', 'uncanonstr', 'websafe', + 'wrap', ] @@ -33,14 +34,15 @@ import logging from email.errors import HeaderParseError from email.header import decode_header, make_header -from string import Template +from string import Template, whitespace +from textwrap import TextWrapper, dedent from zope.component import getUtility from mailman.interfaces.languages import ILanguageManager EMPTYSTRING = '' -UEMPTYSTRING = u'' +NL = '\n' log = logging.getLogger('mailman.error') @@ -91,7 +93,7 @@ def oneline(s, cset='us-ascii', in_unicode=False): try: h = make_header(decode_header(s)) ustr = h.__unicode__() - line = UEMPTYSTRING.join(ustr.splitlines()) + line = EMPTYSTRING.join(ustr.splitlines()) if in_unicode: return line else: @@ -138,3 +140,88 @@ def uncanonstr(s, lang=None): a.append(c) # Join characters together and coerce to byte string return str(EMPTYSTRING.join(a)) + + + +def wrap(text, column=70, honor_leading_ws=True): + """Wrap and fill the text to the specified column. + + The input text is wrapped and filled as done by the standard library + textwrap module. The differences here being that this function is capable + of filling multiple paragraphs (as defined by text separated by blank + lines). Also, when `honor_leading_ws` is True (the default), paragraphs + that being with whitespace are not wrapped. This is the algorithm that + the Python FAQ wizard used. + """ + # First, split the original text into paragraph, keeping all blank lines + # between them. + paragraphs = [] + paragraph = [] + last_indented = False + for line in text.splitlines(True): + is_indented = (len(line) > 0 and line[0] in whitespace) + if line == NL: + if len(paragraph) > 0: + paragraphs.append(EMPTYSTRING.join(paragraph)) + paragraphs.append(line) + last_indented = False + paragraph = [] + elif last_indented != is_indented: + # The indentation level changed. We treat this as a paragraph + # break but no blank line will be issued between paragraphs. + if len(paragraph) > 0: + paragraphs.append(EMPTYSTRING.join(paragraph)) + # The next paragraph starts with this line. + paragraph = [line] + last_indented = is_indented + else: + # This line does not constitute a paragraph break. + paragraph.append(line) + # We've consumed all the lines in the original text. Transfer the last + # paragraph we were collecting to the full set of paragraphs. + paragraphs.append(EMPTYSTRING.join(paragraph)) + # Now iterate through all paragraphs, wrapping as necessary. + wrapped_paragraphs = [] + # The dedented wrapper. + wrapper = TextWrapper(width=column, + fix_sentence_endings=True) + # The indented wrapper. For this one, we'll clobber initial_indent and + # subsequent_indent as needed per indented chunk of text. + iwrapper = TextWrapper(width=column, + fix_sentence_endings=True, + ) + add_paragraph_break = False + for paragraph in paragraphs: + if add_paragraph_break: + wrapped_paragraphs.append(NL) + add_paragraph_break = False + paragraph_text = EMPTYSTRING.join(paragraph) + # Just copy the blank lines to the final set of paragraphs. + if paragraph == NL: + wrapped_paragraphs.append(NL) + # Choose the wrapper based on whether the paragraph is indented or + # not. Also, do not wrap indented paragraphs if honor_leading_ws is + # set. + elif paragraph[0] in whitespace: + if honor_leading_ws: + # Leave the indented paragraph verbatim. + wrapped_paragraphs.append(paragraph_text) + else: + # The paragraph should be wrapped, but it must first be + # dedented. The leading whitespace on the first line of the + # original text will be used as the indentation for all lines + # in the wrapped text. + for i, ch in enumerate(paragraph_text): + if ch not in whitespace: + break + leading_ws = paragraph[:i] + iwrapper.initial_indent=leading_ws + iwrapper.subsequent_indent=leading_ws + paragraph_text = dedent(paragraph_text) + wrapped_paragraphs.append(iwrapper.fill(paragraph_text)) + add_paragraph_break = True + else: + # Fill this paragraph. fill() consumes the trailing newline. + wrapped_paragraphs.append(wrapper.fill(paragraph_text)) + add_paragraph_break = True + return EMPTYSTRING.join(wrapped_paragraphs) diff --git a/src/mailman/utilities/tests/test_templates.py b/src/mailman/utilities/tests/test_templates.py index 2de43ae3c..e21b44544 100644 --- a/src/mailman/utilities/tests/test_templates.py +++ b/src/mailman/utilities/tests/test_templates.py @@ -258,17 +258,15 @@ It will not be wrapped. def test_no_substitutions(self): self.assertEqual(make('nosub.txt', self.mlist), """\ -This is a global template. It has no substitutions. It will be -wrapped. -""") +This is a global template. It has no substitutions. It will be +wrapped.""") def test_substitutions(self): self.assertEqual(make('subs.txt', self.mlist, kind='very nice', howmany='a few'), """\ -This is a very nice template. It has a few substitutions. It will be -wrapped. -""") +This is a very nice template. It has a few substitutions. It will be +wrapped.""") def test_substitutions_no_wrap(self): self.assertEqual(make('nowrap.txt', self.mlist, wrap=False, diff --git a/src/mailman/utilities/tests/test_wrap.py b/src/mailman/utilities/tests/test_wrap.py new file mode 100644 index 000000000..2fc6c6ccf --- /dev/null +++ b/src/mailman/utilities/tests/test_wrap.py @@ -0,0 +1,151 @@ +# Copyright (C) 2011 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test text wrapping.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from mailman.utilities.string import wrap + + + +class TestWrap(unittest.TestCase): + """Test text wrapping.""" + + def test_simple_wrap(self): + text = """\ +This is a single +paragraph. It consists +of several sentences +none of +which are +very long. +""" + self.assertEqual(wrap(text), """\ +This is a single paragraph. It consists of several sentences none of +which are very long.""") + + def test_two_paragraphs(self): + text = """\ +This is a single +paragraph. It consists +of several sentences +none of +which are +very long. + +And here is a second paragraph which +also consists +of several sentences. None of +these are very long +either. +""" + self.assertEqual(wrap(text), """\ +This is a single paragraph. It consists of several sentences none of +which are very long. + +And here is a second paragraph which also consists of several +sentences. None of these are very long either.""") + + def test_honor_ws(self): + text = """\ +This is a single +paragraph. It consists +of several sentences +none of +which are +very long. + + This paragraph is + indented so it + won't be filled. + +And here is a second paragraph which +also consists +of several sentences. None of +these are very long +either. +""" + self.assertEqual(wrap(text), """\ +This is a single paragraph. It consists of several sentences none of +which are very long. + + This paragraph is + indented so it + won't be filled. + +And here is a second paragraph which also consists of several +sentences. None of these are very long either.""") + + def test_dont_honor_ws(self): + text = """\ +This is a single +paragraph. It consists +of several sentences +none of +which are +very long. + + This paragraph is + indented but we don't + honor whitespace so it + will be filled. + +And here is a second paragraph which +also consists +of several sentences. None of +these are very long +either. +""" + self.assertEqual(wrap(text, honor_leading_ws=False), """\ +This is a single paragraph. It consists of several sentences none of +which are very long. + + This paragraph is indented but we don't honor whitespace so it + will be filled. + +And here is a second paragraph which also consists of several +sentences. None of these are very long either.""") + + def test_indentation_boundary(self): + text = """\ +This is a single paragraph +that consists of one sentence. + And another one that breaks + because it is indented. +Followed by one more paragraph. +""" + self.assertEqual(wrap(text), """\ +This is a single paragraph that consists of one sentence. + And another one that breaks + because it is indented. +Followed by one more paragraph.""") + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestWrap)) + return suite |
