浏览代码

Fixed #20223 -- Added keep_lazy() as a replacement for allow_lazy().

Thanks to bmispelon and uruz for the initial patch.
Iacopo Spalletti 9 年之前
父节点
当前提交
d693074d43

+ 2 - 0
AUTHORS

@@ -32,6 +32,7 @@ answer newbie questions, and generally made Django that much better:
     Alex Hill <alex@hill.net.au>
     Alex Ogier <alex.ogier@gmail.com>
     Alex Robbins <alexander.j.robbins@gmail.com>
+    Alexey Boriskin <alex@boriskin.me>
     Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
     Amit Chakradeo <http://amit.chakradeo.net/>
     Amit Ramon <amit.ramon@gmail.com>
@@ -287,6 +288,7 @@ answer newbie questions, and generally made Django that much better:
     Honza Král <honza.kral@gmail.com>
     Horst Gutmann <zerok@zerokspot.com>
     Hyun Mi Ae
+    Iacopo Spalletti <i.spalletti@nephila.it>
     Ian A Wilson <http://ianawilson.com>
     Ian Clelland <clelland@gmail.com>
     Ian G. Kelly <ian.g.kelly@gmail.com>

+ 40 - 10
django/utils/functional.py

@@ -1,8 +1,10 @@
 import copy
 import operator
+import warnings
 from functools import total_ordering, wraps
 
 from django.utils import six
+from django.utils.deprecation import RemovedInDjango20Warning
 
 
 # You can't trivially replace this with `functools.partial` because this binds
@@ -176,24 +178,52 @@ def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
     return lazy(func, *resultclasses)(*args, **kwargs)
 
 
+def lazystr(text):
+    """
+    Shortcut for the common case of a lazy callable that returns str.
+    """
+    from django.utils.encoding import force_text  # Avoid circular import
+    return lazy(force_text, six.text_type)(text)
+
+
 def allow_lazy(func, *resultclasses):
+    warnings.warn(
+        "django.utils.functional.allow_lazy() is deprecated in favor of "
+        "django.utils.functional.keep_lazy()",
+        RemovedInDjango20Warning, 2)
+    return keep_lazy(*resultclasses)(func)
+
+
+def keep_lazy(*resultclasses):
     """
     A decorator that allows a function to be called with one or more lazy
     arguments. If none of the args are lazy, the function is evaluated
     immediately, otherwise a __proxy__ is returned that will evaluate the
     function when needed.
     """
-    lazy_func = lazy(func, *resultclasses)
+    if not resultclasses:
+        raise TypeError("You must pass at least one argument to keep_lazy().")
 
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        for arg in list(args) + list(kwargs.values()):
-            if isinstance(arg, Promise):
-                break
-        else:
-            return func(*args, **kwargs)
-        return lazy_func(*args, **kwargs)
-    return wrapper
+    def decorator(func):
+        lazy_func = lazy(func, *resultclasses)
+
+        @wraps(func)
+        def wrapper(*args, **kwargs):
+            for arg in list(args) + list(six.itervalues(kwargs)):
+                if isinstance(arg, Promise):
+                    break
+            else:
+                return func(*args, **kwargs)
+            return lazy_func(*args, **kwargs)
+        return wrapper
+    return decorator
+
+
+def keep_lazy_text(func):
+    """
+    A decorator for functions that accept lazy arguments and return text.
+    """
+    return keep_lazy(six.text_type)(func)
 
 empty = object()
 

+ 9 - 8
django/utils/html.py

@@ -6,7 +6,7 @@ import re
 
 from django.utils import six
 from django.utils.encoding import force_str, force_text
-from django.utils.functional import allow_lazy
+from django.utils.functional import keep_lazy, keep_lazy_text
 from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
 from django.utils.safestring import SafeData, SafeText, mark_safe
 from django.utils.six.moves.urllib.parse import (
@@ -38,6 +38,7 @@ hard_coded_bullets_re = re.compile(
 trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
 
 
+@keep_lazy(six.text_type, SafeText)
 def escape(text):
     """
     Returns the given text with ampersands, quotes and angle brackets encoded
@@ -49,7 +50,6 @@ def escape(text):
     """
     return mark_safe(force_text(text).replace('&', '&amp;').replace('<', '&lt;')
         .replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
-escape = allow_lazy(escape, six.text_type, SafeText)
 
 _js_escapes = {
     ord('\\'): '\\u005C',
@@ -69,10 +69,10 @@ _js_escapes = {
 _js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))
 
 
+@keep_lazy(six.text_type, SafeText)
 def escapejs(value):
     """Hex encodes characters for use in JavaScript strings."""
     return mark_safe(force_text(value).translate(_js_escapes))
-escapejs = allow_lazy(escapejs, six.text_type, SafeText)
 
 
 def conditional_escape(text):
@@ -118,16 +118,16 @@ def format_html_join(sep, format_string, args_generator):
         for args in args_generator))
 
 
+@keep_lazy_text
 def linebreaks(value, autoescape=False):
     """Converts newlines into <p> and <br />s."""
-    value = normalize_newlines(value)
+    value = normalize_newlines(force_text(value))
     paras = re.split('\n{2,}', value)
     if autoescape:
         paras = ['<p>%s</p>' % escape(p).replace('\n', '<br />') for p in paras]
     else:
         paras = ['<p>%s</p>' % p.replace('\n', '<br />') for p in paras]
     return '\n\n'.join(paras)
-linebreaks = allow_lazy(linebreaks, six.text_type)
 
 
 class MLStripper(HTMLParser):
@@ -166,10 +166,12 @@ def _strip_once(value):
         return s.get_data()
 
 
+@keep_lazy_text
 def strip_tags(value):
     """Returns the given HTML with all tags stripped."""
     # Note: in typical case this loop executes _strip_once once. Loop condition
     # is redundant, but helps to reduce number of executions of _strip_once.
+    value = force_text(value)
     while '<' in value and '>' in value:
         new_value = _strip_once(value)
         if len(new_value) >= len(value):
@@ -179,13 +181,12 @@ def strip_tags(value):
             break
         value = new_value
     return value
-strip_tags = allow_lazy(strip_tags)
 
 
+@keep_lazy_text
 def strip_spaces_between_tags(value):
     """Returns the given HTML with spaces between tags removed."""
     return re.sub(r'>\s+<', '><', force_text(value))
-strip_spaces_between_tags = allow_lazy(strip_spaces_between_tags, six.text_type)
 
 
 def smart_urlquote(url):
@@ -224,6 +225,7 @@ def smart_urlquote(url):
     return urlunsplit((scheme, netloc, path, query, fragment))
 
 
+@keep_lazy_text
 def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
     """
     Converts any URLs in text into clickable links.
@@ -321,7 +323,6 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
         elif autoescape:
             words[i] = escape(word)
     return ''.join(words)
-urlize = allow_lazy(urlize, six.text_type)
 
 
 def avoid_wrapping(value):

+ 5 - 5
django/utils/http.py

@@ -12,7 +12,7 @@ from email.utils import formatdate
 from django.utils import six
 from django.utils.datastructures import MultiValueDict
 from django.utils.encoding import force_bytes, force_str, force_text
-from django.utils.functional import allow_lazy
+from django.utils.functional import keep_lazy_text
 from django.utils.six.moves.urllib.parse import (
     quote, quote_plus, unquote, unquote_plus, urlencode as original_urlencode,
     urlparse,
@@ -40,6 +40,7 @@ PROTOCOL_TO_PORT = {
 }
 
 
+@keep_lazy_text
 def urlquote(url, safe='/'):
     """
     A version of Python's urllib.quote() function that can operate on unicode
@@ -48,9 +49,9 @@ def urlquote(url, safe='/'):
     without double-quoting occurring.
     """
     return force_text(quote(force_str(url), force_str(safe)))
-urlquote = allow_lazy(urlquote, six.text_type)
 
 
+@keep_lazy_text
 def urlquote_plus(url, safe=''):
     """
     A version of Python's urllib.quote_plus() function that can operate on
@@ -59,25 +60,24 @@ def urlquote_plus(url, safe=''):
     iri_to_uri() call without double-quoting occurring.
     """
     return force_text(quote_plus(force_str(url), force_str(safe)))
-urlquote_plus = allow_lazy(urlquote_plus, six.text_type)
 
 
+@keep_lazy_text
 def urlunquote(quoted_url):
     """
     A wrapper for Python's urllib.unquote() function that can operate on
     the result of django.utils.http.urlquote().
     """
     return force_text(unquote(force_str(quoted_url)))
-urlunquote = allow_lazy(urlunquote, six.text_type)
 
 
+@keep_lazy_text
 def urlunquote_plus(quoted_url):
     """
     A wrapper for Python's urllib.unquote_plus() function that can operate on
     the result of django.utils.http.urlquote_plus().
     """
     return force_text(unquote_plus(force_str(quoted_url)))
-urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type)
 
 
 def urlencode(query, doseq=0):

+ 13 - 13
django/utils/text.py

@@ -7,7 +7,7 @@ from io import BytesIO
 
 from django.utils import six
 from django.utils.encoding import force_text
-from django.utils.functional import SimpleLazyObject, allow_lazy
+from django.utils.functional import SimpleLazyObject, keep_lazy, keep_lazy_text
 from django.utils.safestring import SafeText, mark_safe
 from django.utils.six.moves import html_entities
 from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
@@ -20,7 +20,7 @@ if six.PY2:
 
 # Capitalizes the first letter of a string.
 capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:]
-capfirst = allow_lazy(capfirst, six.text_type)
+capfirst = keep_lazy_text(capfirst)
 
 # Set up regular expressions
 re_words = re.compile(r'<.*?>|((?:\w[-\w]*|&.*?;)+)', re.U | re.S)
@@ -30,6 +30,7 @@ re_newlines = re.compile(r'\r\n|\r')  # Used in normalize_newlines
 re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
 
 
+@keep_lazy_text
 def wrap(text, width):
     """
     A word-wrap function that preserves existing line breaks. Expects that
@@ -60,7 +61,6 @@ def wrap(text, width):
             if line:
                 yield line
     return ''.join(_generator())
-wrap = allow_lazy(wrap, six.text_type)
 
 
 class Truncator(SimpleLazyObject):
@@ -95,6 +95,7 @@ class Truncator(SimpleLazyObject):
         string has been truncated, defaulting to a translatable string of an
         ellipsis (...).
         """
+        self._setup()
         length = int(num)
         text = unicodedata.normalize('NFC', self._wrapped)
 
@@ -108,7 +109,6 @@ class Truncator(SimpleLazyObject):
         if html:
             return self._truncate_html(length, truncate, text, truncate_len, False)
         return self._text_chars(length, truncate, text, truncate_len)
-    chars = allow_lazy(chars)
 
     def _text_chars(self, length, truncate, text, truncate_len):
         """
@@ -138,11 +138,11 @@ class Truncator(SimpleLazyObject):
         argument of what should be used to notify that the string has been
         truncated, defaulting to ellipsis (...).
         """
+        self._setup()
         length = int(num)
         if html:
             return self._truncate_html(length, truncate, self._wrapped, length, True)
         return self._text_words(length, truncate)
-    words = allow_lazy(words)
 
     def _text_words(self, length, truncate):
         """
@@ -229,6 +229,7 @@ class Truncator(SimpleLazyObject):
         return out
 
 
+@keep_lazy_text
 def get_valid_filename(s):
     """
     Returns the given string converted to a string that can be used for a clean
@@ -240,9 +241,9 @@ def get_valid_filename(s):
     """
     s = force_text(s).strip().replace(' ', '_')
     return re.sub(r'(?u)[^-\w.]', '', s)
-get_valid_filename = allow_lazy(get_valid_filename, six.text_type)
 
 
+@keep_lazy_text
 def get_text_list(list_, last_word=ugettext_lazy('or')):
     """
     >>> get_text_list(['a', 'b', 'c', 'd'])
@@ -264,16 +265,16 @@ def get_text_list(list_, last_word=ugettext_lazy('or')):
         # Translators: This string is used as a separator between list elements
         _(', ').join(force_text(i) for i in list_[:-1]),
         force_text(last_word), force_text(list_[-1]))
-get_text_list = allow_lazy(get_text_list, six.text_type)
 
 
+@keep_lazy_text
 def normalize_newlines(text):
     """Normalizes CRLF and CR newlines to just LF."""
     text = force_text(text)
     return re_newlines.sub('\n', text)
-normalize_newlines = allow_lazy(normalize_newlines, six.text_type)
 
 
+@keep_lazy_text
 def phone2numeric(phone):
     """Converts a phone number with letters into its numeric equivalent."""
     char2number = {'a': '2', 'b': '2', 'c': '2', 'd': '3', 'e': '3', 'f': '3',
@@ -281,7 +282,6 @@ def phone2numeric(phone):
          'n': '6', 'o': '6', 'p': '7', 'q': '7', 'r': '7', 's': '7', 't': '8',
          'u': '8', 'v': '8', 'w': '9', 'x': '9', 'y': '9', 'z': '9'}
     return ''.join(char2number.get(c, c) for c in phone.lower())
-phone2numeric = allow_lazy(phone2numeric)
 
 
 # From http://www.xhaus.com/alan/python/httpcomp.html#gzip
@@ -384,11 +384,12 @@ def _replace_entity(match):
 _entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
 
 
+@keep_lazy_text
 def unescape_entities(text):
-    return _entity_re.sub(_replace_entity, text)
-unescape_entities = allow_lazy(unescape_entities, six.text_type)
+    return _entity_re.sub(_replace_entity, force_text(text))
 
 
+@keep_lazy_text
 def unescape_string_literal(s):
     r"""
     Convert quoted string literals to unquoted strings with escaped quotes and
@@ -407,9 +408,9 @@ def unescape_string_literal(s):
         raise ValueError("Not a string literal: %r" % s)
     quote = s[0]
     return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
-unescape_string_literal = allow_lazy(unescape_string_literal)
 
 
+@keep_lazy(six.text_type, SafeText)
 def slugify(value, allow_unicode=False):
     """
     Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens.
@@ -424,7 +425,6 @@ def slugify(value, allow_unicode=False):
     value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
     value = re.sub('[^\w\s-]', '', value).strip().lower()
     return mark_safe(re.sub('[-\s]+', '-', value))
-slugify = allow_lazy(slugify, six.text_type, SafeText)
 
 
 def camel_case_to_spaces(value):

+ 2 - 0
docs/internals/deprecation.txt

@@ -124,6 +124,8 @@ details on these changes.
 * The ``cascaded_union`` property of ``django.contrib.gis.geos.MultiPolygon``
   will be removed.
 
+* ``django.utils.functional.allow_lazy()`` will be removed.
+
 .. _deprecation-removed-in-1.10:
 
 1.10

+ 45 - 9
docs/ref/utils.txt

@@ -522,6 +522,15 @@ Atom1Feed
 
 .. function:: allow_lazy(func, *resultclasses)
 
+    .. deprecated:: 1.10
+
+    Works like :meth:`~django.utils.functional.keep_lazy` except that it can't
+    be used as a decorator.
+
+.. function:: keep_lazy(func, *resultclasses)
+
+    .. versionadded:: 1.10
+
     Django offers many utility functions (particularly in ``django.utils``)
     that take a string as their first argument and do something to that string.
     These functions are used by template filters as well as directly in other
@@ -533,31 +542,58 @@ Atom1Feed
     because you might be using this function outside of a view (and hence the
     current thread's locale setting will not be correct).
 
-    For cases like this, use the ``django.utils.functional.allow_lazy()``
+    For cases like this, use the ``django.utils.functional.keep_lazy()``
     decorator. It modifies the function so that *if* it's called with a lazy
     translation as one of its arguments, the function evaluation is delayed
     until it needs to be converted to a string.
 
     For example::
 
-        from django.utils.functional import allow_lazy
+        from django.utils import six
+        from django.utils.functional import keep_lazy, keep_lazy_text
 
         def fancy_utility_function(s, ...):
             # Do some conversion on string 's'
             ...
-        # Replace unicode by str on Python 3
-        fancy_utility_function = allow_lazy(fancy_utility_function, unicode)
+        fancy_utility_function = keep_lazy(six.text_type)(fancy_utility_function)
 
-    The ``allow_lazy()`` decorator takes, in addition to the function to
-    decorate, a number of extra arguments (``*args``) specifying the type(s)
-    that the original function can return. Usually, it's enough to include
-    ``unicode`` (or ``str`` on Python 3) here and ensure that your function
-    returns only Unicode strings.
+        # Or more succinctly:
+        @keep_lazy(six.text_type)
+        def fancy_utility_function(s, ...):
+            ...
+
+    The ``keep_lazy()`` decorator takes a number of extra arguments (``*args``)
+    specifying the type(s) that the original function can return. A common
+    use case is to have functions that return text. For these, you can just
+    pass the ``six.text_type`` type to ``keep_lazy`` (or even simpler, use the
+    :func:`keep_lazy_text` decorator described in the next section).
 
     Using this decorator means you can write your function and assume that the
     input is a proper string, then add support for lazy translation objects at
     the end.
 
+.. function:: keep_lazy_text(func)
+
+    .. versionadded:: 1.10
+
+    A shortcut for ``keep_lazy(six.text_type)(func)``.
+
+    If you have a function that returns text and you want to be able to take
+    lazy arguments while delaying their evaluation, simply use this decorator::
+
+        from django.utils import six
+        from django.utils.functional import keep_lazy, keep_lazy_text
+
+        # Our previous example was:
+        @keep_lazy(six.text_type)
+        def fancy_utility_function(s, ...):
+            ...
+
+        # Which can be rewritten as:
+        @keep_lazy_text
+        def fancy_utility_function(s, ...):
+            ...
+
 ``django.utils.html``
 =====================
 

+ 4 - 0
docs/releases/1.10.txt

@@ -402,6 +402,10 @@ Miscellaneous
 * The ``makemigrations --exit`` option is deprecated in favor of the
   :djadminopt:`--check` option.
 
+* ``django.utils.functional.allow_lazy()`` is deprecated in favor of the new
+  :func:`~django.utils.functional.keep_lazy` function which can be used with a
+  more natural decorator syntax.
+
 .. _removed-features-1.10:
 
 Features removed in 1.10

+ 1 - 1
docs/topics/performance.txt

@@ -223,7 +223,7 @@ QuerySet <when-querysets-are-evaluated>`. Avoiding the premature evaluation of
 a ``QuerySet`` can save making an expensive and unnecessary trip to the
 database.
 
-Django also offers an :meth:`~django.utils.functional.allow_lazy` decorator.
+Django also offers a :meth:`~django.utils.functional.keep_lazy` decorator.
 This allows a function that has been called with a lazy argument to behave
 lazily itself, only being evaluated when it needs to be. Thus the lazy argument
 - which could be an expensive one - will not be called upon for evaluation

+ 16 - 2
tests/decorators/tests.py

@@ -8,8 +8,12 @@ from django.contrib.auth.decorators import (
 from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
 from django.middleware.clickjacking import XFrameOptionsMiddleware
 from django.test import SimpleTestCase
+from django.utils import six
 from django.utils.decorators import method_decorator
-from django.utils.functional import allow_lazy, lazy
+from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.encoding import force_text
+from django.utils.functional import allow_lazy, keep_lazy, keep_lazy_text, lazy
+from django.utils.translation import ugettext_lazy
 from django.views.decorators.cache import (
     cache_control, cache_page, never_cache,
 )
@@ -67,7 +71,8 @@ full_decorator = compose(
     staff_member_required,
 
     # django.utils.functional
-    allow_lazy,
+    keep_lazy(HttpResponse),
+    keep_lazy_text,
     lazy,
 )
 
@@ -149,6 +154,15 @@ class DecoratorsTest(TestCase):
         request.method = 'DELETE'
         self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed)
 
+    def test_deprecated_allow_lazy(self):
+        with self.assertRaises(RemovedInDjango20Warning):
+            def noop_text(text):
+                return force_text(text)
+            noop_text = allow_lazy(noop_text, six.text_type)
+            rendered = noop_text(ugettext_lazy("I am a text"))
+            self.assertEqual(type(rendered), six.text_type)
+            self.assertEqual(rendered, "I am a text")
+
 
 # For testing method_decorator, a decorator that assumes a single argument.
 # We will get type arguments if there is a mismatch in the number of arguments.

+ 2 - 4
tests/httpwrappers/tests.py

@@ -21,10 +21,8 @@ from django.http import (
 from django.test import SimpleTestCase
 from django.utils import six
 from django.utils._os import upath
-from django.utils.encoding import force_text, smart_str
-from django.utils.functional import lazy
-
-lazystr = lazy(force_text, six.text_type)
+from django.utils.encoding import smart_str
+from django.utils.functional import lazystr
 
 
 class QueryDictTests(unittest.TestCase):

+ 8 - 0
tests/template_tests/filter_tests/test_escape.py

@@ -1,5 +1,7 @@
 from django.template.defaultfilters import escape
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import Promise, lazy
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -33,6 +35,12 @@ class EscapeTests(SimpleTestCase):
         output = self.engine.render_to_string('escape04', {"a": "x&y"})
         self.assertEqual(output, "x&amp;y")
 
+    def test_escape_lazy_string(self):
+        add_html = lazy(lambda string: string + 'special characters > here', six.text_type)
+        escaped = escape(add_html('<some html & '))
+        self.assertIsInstance(escaped, Promise)
+        self.assertEqual(escaped, '&lt;some html &amp; special characters &gt; here')
+
 
 class FunctionTests(SimpleTestCase):
 

+ 10 - 0
tests/template_tests/filter_tests/test_escapejs.py

@@ -2,6 +2,8 @@ from __future__ import unicode_literals
 
 from django.template.defaultfilters import escapejs_filter
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 
 from ..utils import setup
 
@@ -51,3 +53,11 @@ class FunctionTests(SimpleTestCase):
             escapejs_filter('paragraph separator:\u2029and line separator:\u2028'),
             'paragraph separator:\\u2029and line separator:\\u2028',
         )
+
+    def test_lazy_string(self):
+        append_script = lazy(lambda string: r'<script>this</script>' + string, six.text_type)
+        self.assertEqual(
+            escapejs_filter(append_script('whitespace: \r\n\t\v\f\b')),
+            '\\u003Cscript\\u003Ethis\\u003C/script\\u003E'
+            'whitespace: \\u000D\\u000A\\u0009\\u000B\\u000C\\u0008'
+        )

+ 9 - 0
tests/template_tests/filter_tests/test_linebreaks.py

@@ -1,5 +1,7 @@
 from django.template.defaultfilters import linebreaks_filter
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -51,3 +53,10 @@ class FunctionTests(SimpleTestCase):
             linebreaks_filter('foo\n<a>bar</a>\nbuz', autoescape=False),
             '<p>foo<br /><a>bar</a><br />buz</p>',
         )
+
+    def test_lazy_string_input(self):
+        add_header = lazy(lambda string: 'Header\n\n' + string, six.text_type)
+        self.assertEqual(
+            linebreaks_filter(add_header('line 1\r\nline2')),
+            '<p>Header</p>\n\n<p>line 1<br />line2</p>'
+        )

+ 10 - 0
tests/template_tests/filter_tests/test_slugify.py

@@ -3,6 +3,9 @@ from __future__ import unicode_literals
 
 from django.template.defaultfilters import slugify
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.encoding import force_text
+from django.utils.functional import lazy
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -41,3 +44,10 @@ class FunctionTests(SimpleTestCase):
 
     def test_non_string_input(self):
         self.assertEqual(slugify(123), '123')
+
+    def test_slugify_lazy_string(self):
+        lazy_str = lazy(lambda string: force_text(string), six.text_type)
+        self.assertEqual(
+            slugify(lazy_str(' Jack & Jill like numbers 1,2,3 and 4 and silly characters ?%.$!/')),
+            'jack-jill-like-numbers-123-and-4-and-silly-characters',
+        )

+ 7 - 0
tests/template_tests/filter_tests/test_striptags.py

@@ -1,5 +1,6 @@
 from django.template.defaultfilters import striptags
 from django.test import SimpleTestCase
+from django.utils.functional import lazystr
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -40,3 +41,9 @@ class FunctionTests(SimpleTestCase):
 
     def test_non_string_input(self):
         self.assertEqual(striptags(123), '123')
+
+    def test_strip_lazy_string(self):
+        self.assertEqual(
+            striptags(lazystr('some <b>html</b> with <script>alert("Hello")</script> disallowed <img /> tags')),
+            'some html with alert("Hello") disallowed  tags',
+        )

+ 9 - 0
tests/template_tests/filter_tests/test_urlize.py

@@ -3,6 +3,8 @@ from __future__ import unicode_literals
 
 from django.template.defaultfilters import urlize
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -348,3 +350,10 @@ class FunctionTests(SimpleTestCase):
             urlize('foo<a href=" google.com ">bar</a>buz', autoescape=False),
             'foo<a href=" <a href="http://google.com" rel="nofollow">google.com</a> ">bar</a>buz',
         )
+
+    def test_lazystring(self):
+        prepend_www = lazy(lambda url: 'www.' + url, six.text_type)
+        self.assertEqual(
+            urlize(prepend_www('google.com')),
+            '<a href="http://www.google.com" rel="nofollow">www.google.com</a>',
+        )

+ 9 - 0
tests/template_tests/filter_tests/test_wordwrap.py

@@ -1,5 +1,6 @@
 from django.template.defaultfilters import wordwrap
 from django.test import SimpleTestCase
+from django.utils.functional import lazystr
 from django.utils.safestring import mark_safe
 
 from ..utils import setup
@@ -41,3 +42,11 @@ class FunctionTests(SimpleTestCase):
 
     def test_non_string_input(self):
         self.assertEqual(wordwrap(123, 2), '123')
+
+    def test_wrap_lazy_string(self):
+        self.assertEqual(
+            wordwrap(lazystr(
+                'this is a long paragraph of text that really needs to be wrapped I\'m afraid'
+            ), 14),
+            'this is a long\nparagraph of\ntext that\nreally needs\nto be wrapped\nI\'m afraid',
+        )

+ 7 - 0
tests/utils_tests/test_html.py

@@ -8,6 +8,7 @@ from django.test import SimpleTestCase
 from django.utils import html, safestring, six
 from django.utils._os import upath
 from django.utils.encoding import force_text
+from django.utils.functional import lazystr
 
 
 class TestUtilsHtml(SimpleTestCase):
@@ -35,6 +36,7 @@ class TestUtilsHtml(SimpleTestCase):
         for value, output in items:
             for pattern in patterns:
                 self.check_output(f, pattern % value, pattern % output)
+                self.check_output(f, lazystr(pattern % value), pattern % output)
             # Check repeated values.
             self.check_output(f, value * 2, output * 2)
         # Verify it doesn't double replace &.
@@ -61,6 +63,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         for value, output in items:
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
     def test_strip_tags(self):
         f = html.strip_tags
@@ -86,6 +89,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         for value, output in items:
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
         # Some convoluted syntax for which parsing may differ between python versions
         output = html.strip_tags('<sc<!-- -->ript>test<<!-- -->/script>')
@@ -113,6 +117,7 @@ class TestUtilsHtml(SimpleTestCase):
         items = (' <adf>', '<adf> ', ' </adf> ', ' <f> x</f>')
         for value in items:
             self.check_output(f, value)
+            self.check_output(f, lazystr(value))
         # Strings that have spaces to strip.
         items = (
             ('<d> </d>', '<d></d>'),
@@ -121,6 +126,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         for value, output in items:
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
     def test_escapejs(self):
         f = html.escapejs
@@ -139,6 +145,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         for value, output in items:
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
     def test_smart_urlquote(self):
         quote = html.smart_urlquote

+ 2 - 3
tests/utils_tests/test_safestring.py

@@ -3,13 +3,12 @@ from __future__ import unicode_literals
 from django.template import Context, Template
 from django.test import SimpleTestCase
 from django.utils import html, six, text
-from django.utils.encoding import force_bytes, force_text
-from django.utils.functional import lazy
+from django.utils.encoding import force_bytes
+from django.utils.functional import lazy, lazystr
 from django.utils.safestring import (
     EscapeData, SafeData, mark_for_escaping, mark_safe,
 )
 
-lazystr = lazy(force_text, six.text_type)
 lazybytes = lazy(force_bytes, bytes)
 
 

+ 27 - 4
tests/utils_tests/test_text.py

@@ -5,12 +5,9 @@ import json
 
 from django.test import SimpleTestCase
 from django.utils import six, text
-from django.utils.encoding import force_text
-from django.utils.functional import lazy
+from django.utils.functional import lazystr
 from django.utils.translation import override
 
-lazystr = lazy(force_text, six.text_type)
-
 IS_WIDE_BUILD = (len('\U0001F4A9') == 1)
 
 
@@ -93,6 +90,8 @@ class TestUtilsText(SimpleTestCase):
         # Make a best effort to shorten to the desired length, but requesting
         # a length shorter than the ellipsis shouldn't break
         self.assertEqual('...', text.Truncator('asdf').chars(1))
+        # Ensure that lazy strings are handled correctly
+        self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(12), 'The quick...')
 
     def test_truncate_words(self):
         truncator = text.Truncator('The quick brown fox jumped over the lazy '
@@ -102,6 +101,9 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual('The quick brown fox...', truncator.words(4))
         self.assertEqual('The quick brown fox[snip]',
             truncator.words(4, '[snip]'))
+        # Ensure that lazy strings are handled correctly
+        truncator = text.Truncator(lazystr('The quick brown fox jumped over the lazy dog.'))
+        self.assertEqual('The quick brown fox...', truncator.words(4))
 
     def test_truncate_html_words(self):
         truncator = text.Truncator('<p id="par"><strong><em>The quick brown fox'
@@ -156,6 +158,7 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual(text.wrap(long_word, 20), long_word)
         self.assertEqual(text.wrap('a %s word' % long_word, 10),
                          'a\n%s\nword' % long_word)
+        self.assertEqual(text.wrap(lazystr(digits), 100), '1234 67 9')
 
     def test_normalize_newlines(self):
         self.assertEqual(text.normalize_newlines("abc\ndef\rghi\r\n"),
@@ -163,6 +166,7 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual(text.normalize_newlines("\n\r\r\n\r"), "\n\n\n\n")
         self.assertEqual(text.normalize_newlines("abcdefghi"), "abcdefghi")
         self.assertEqual(text.normalize_newlines(""), "")
+        self.assertEqual(text.normalize_newlines(lazystr("abc\ndef\rghi\r\n")), "abc\ndef\nghi\n")
 
     def test_normalize_newlines_bytes(self):
         """normalize_newlines should be able to handle bytes too"""
@@ -170,6 +174,12 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual(normalized, "abc\ndef\nghi\n")
         self.assertIsInstance(normalized, six.text_type)
 
+    def test_phone2numeric(self):
+        numeric = text.phone2numeric('0800 flowers')
+        self.assertEqual(numeric, '0800 3569377')
+        lazy_numeric = lazystr(text.phone2numeric('0800 flowers'))
+        self.assertEqual(lazy_numeric, '0800 3569377')
+
     def test_slugify(self):
         items = (
             # given - expected - unicode?
@@ -195,10 +205,23 @@ class TestUtilsText(SimpleTestCase):
         ]
         for value, output in items:
             self.assertEqual(text.unescape_entities(value), output)
+            self.assertEqual(text.unescape_entities(lazystr(value)), output)
+
+    def test_unescape_string_literal(self):
+        items = [
+            ('"abc"', 'abc'),
+            ("'abc'", 'abc'),
+            ('"a \"bc\""', 'a "bc"'),
+            ("'\'ab\' c'", "'ab' c"),
+        ]
+        for value, output in items:
+            self.assertEqual(text.unescape_string_literal(value), output)
+            self.assertEqual(text.unescape_string_literal(lazystr(value)), output)
 
     def test_get_valid_filename(self):
         filename = "^&'@{}[],$=!-#()%+~_123.txt"
         self.assertEqual(text.get_valid_filename(filename), "-_123.txt")
+        self.assertEqual(text.get_valid_filename(lazystr(filename)), "-_123.txt")
 
     def test_compress_sequence(self):
         data = [{'key': i} for i in range(10)]