Browse Source

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

Thanks to bmispelon and uruz for the initial patch.
Iacopo Spalletti 9 years ago
parent
commit
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 Hill <alex@hill.net.au>
     Alex Ogier <alex.ogier@gmail.com>
     Alex Ogier <alex.ogier@gmail.com>
     Alex Robbins <alexander.j.robbins@gmail.com>
     Alex Robbins <alexander.j.robbins@gmail.com>
+    Alexey Boriskin <alex@boriskin.me>
     Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
     Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
     Amit Chakradeo <http://amit.chakradeo.net/>
     Amit Chakradeo <http://amit.chakradeo.net/>
     Amit Ramon <amit.ramon@gmail.com>
     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>
     Honza Král <honza.kral@gmail.com>
     Horst Gutmann <zerok@zerokspot.com>
     Horst Gutmann <zerok@zerokspot.com>
     Hyun Mi Ae
     Hyun Mi Ae
+    Iacopo Spalletti <i.spalletti@nephila.it>
     Ian A Wilson <http://ianawilson.com>
     Ian A Wilson <http://ianawilson.com>
     Ian Clelland <clelland@gmail.com>
     Ian Clelland <clelland@gmail.com>
     Ian G. Kelly <ian.g.kelly@gmail.com>
     Ian G. Kelly <ian.g.kelly@gmail.com>

+ 40 - 10
django/utils/functional.py

@@ -1,8 +1,10 @@
 import copy
 import copy
 import operator
 import operator
+import warnings
 from functools import total_ordering, wraps
 from functools import total_ordering, wraps
 
 
 from django.utils import six
 from django.utils import six
+from django.utils.deprecation import RemovedInDjango20Warning
 
 
 
 
 # You can't trivially replace this with `functools.partial` because this binds
 # 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)
     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):
 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
     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
     arguments. If none of the args are lazy, the function is evaluated
     immediately, otherwise a __proxy__ is returned that will evaluate the
     immediately, otherwise a __proxy__ is returned that will evaluate the
     function when needed.
     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()
 empty = object()
 
 

+ 9 - 8
django/utils/html.py

@@ -6,7 +6,7 @@ import re
 
 
 from django.utils import six
 from django.utils import six
 from django.utils.encoding import force_str, force_text
 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.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
 from django.utils.safestring import SafeData, SafeText, mark_safe
 from django.utils.safestring import SafeData, SafeText, mark_safe
 from django.utils.six.moves.urllib.parse import (
 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')
 trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
 
 
 
 
+@keep_lazy(six.text_type, SafeText)
 def escape(text):
 def escape(text):
     """
     """
     Returns the given text with ampersands, quotes and angle brackets encoded
     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;')
     return mark_safe(force_text(text).replace('&', '&amp;').replace('<', '&lt;')
         .replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
         .replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
-escape = allow_lazy(escape, six.text_type, SafeText)
 
 
 _js_escapes = {
 _js_escapes = {
     ord('\\'): '\\u005C',
     ord('\\'): '\\u005C',
@@ -69,10 +69,10 @@ _js_escapes = {
 _js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))
 _js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))
 
 
 
 
+@keep_lazy(six.text_type, SafeText)
 def escapejs(value):
 def escapejs(value):
     """Hex encodes characters for use in JavaScript strings."""
     """Hex encodes characters for use in JavaScript strings."""
     return mark_safe(force_text(value).translate(_js_escapes))
     return mark_safe(force_text(value).translate(_js_escapes))
-escapejs = allow_lazy(escapejs, six.text_type, SafeText)
 
 
 
 
 def conditional_escape(text):
 def conditional_escape(text):
@@ -118,16 +118,16 @@ def format_html_join(sep, format_string, args_generator):
         for args in args_generator))
         for args in args_generator))
 
 
 
 
+@keep_lazy_text
 def linebreaks(value, autoescape=False):
 def linebreaks(value, autoescape=False):
     """Converts newlines into <p> and <br />s."""
     """Converts newlines into <p> and <br />s."""
-    value = normalize_newlines(value)
+    value = normalize_newlines(force_text(value))
     paras = re.split('\n{2,}', value)
     paras = re.split('\n{2,}', value)
     if autoescape:
     if autoescape:
         paras = ['<p>%s</p>' % escape(p).replace('\n', '<br />') for p in paras]
         paras = ['<p>%s</p>' % escape(p).replace('\n', '<br />') for p in paras]
     else:
     else:
         paras = ['<p>%s</p>' % p.replace('\n', '<br />') for p in paras]
         paras = ['<p>%s</p>' % p.replace('\n', '<br />') for p in paras]
     return '\n\n'.join(paras)
     return '\n\n'.join(paras)
-linebreaks = allow_lazy(linebreaks, six.text_type)
 
 
 
 
 class MLStripper(HTMLParser):
 class MLStripper(HTMLParser):
@@ -166,10 +166,12 @@ def _strip_once(value):
         return s.get_data()
         return s.get_data()
 
 
 
 
+@keep_lazy_text
 def strip_tags(value):
 def strip_tags(value):
     """Returns the given HTML with all tags stripped."""
     """Returns the given HTML with all tags stripped."""
     # Note: in typical case this loop executes _strip_once once. Loop condition
     # Note: in typical case this loop executes _strip_once once. Loop condition
     # is redundant, but helps to reduce number of executions of _strip_once.
     # is redundant, but helps to reduce number of executions of _strip_once.
+    value = force_text(value)
     while '<' in value and '>' in value:
     while '<' in value and '>' in value:
         new_value = _strip_once(value)
         new_value = _strip_once(value)
         if len(new_value) >= len(value):
         if len(new_value) >= len(value):
@@ -179,13 +181,12 @@ def strip_tags(value):
             break
             break
         value = new_value
         value = new_value
     return value
     return value
-strip_tags = allow_lazy(strip_tags)
 
 
 
 
+@keep_lazy_text
 def strip_spaces_between_tags(value):
 def strip_spaces_between_tags(value):
     """Returns the given HTML with spaces between tags removed."""
     """Returns the given HTML with spaces between tags removed."""
     return re.sub(r'>\s+<', '><', force_text(value))
     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):
 def smart_urlquote(url):
@@ -224,6 +225,7 @@ def smart_urlquote(url):
     return urlunsplit((scheme, netloc, path, query, fragment))
     return urlunsplit((scheme, netloc, path, query, fragment))
 
 
 
 
+@keep_lazy_text
 def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
 def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
     """
     """
     Converts any URLs in text into clickable links.
     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:
         elif autoescape:
             words[i] = escape(word)
             words[i] = escape(word)
     return ''.join(words)
     return ''.join(words)
-urlize = allow_lazy(urlize, six.text_type)
 
 
 
 
 def avoid_wrapping(value):
 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 import six
 from django.utils.datastructures import MultiValueDict
 from django.utils.datastructures import MultiValueDict
 from django.utils.encoding import force_bytes, force_str, force_text
 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 (
 from django.utils.six.moves.urllib.parse import (
     quote, quote_plus, unquote, unquote_plus, urlencode as original_urlencode,
     quote, quote_plus, unquote, unquote_plus, urlencode as original_urlencode,
     urlparse,
     urlparse,
@@ -40,6 +40,7 @@ PROTOCOL_TO_PORT = {
 }
 }
 
 
 
 
+@keep_lazy_text
 def urlquote(url, safe='/'):
 def urlquote(url, safe='/'):
     """
     """
     A version of Python's urllib.quote() function that can operate on unicode
     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.
     without double-quoting occurring.
     """
     """
     return force_text(quote(force_str(url), force_str(safe)))
     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=''):
 def urlquote_plus(url, safe=''):
     """
     """
     A version of Python's urllib.quote_plus() function that can operate on
     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.
     iri_to_uri() call without double-quoting occurring.
     """
     """
     return force_text(quote_plus(force_str(url), force_str(safe)))
     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):
 def urlunquote(quoted_url):
     """
     """
     A wrapper for Python's urllib.unquote() function that can operate on
     A wrapper for Python's urllib.unquote() function that can operate on
     the result of django.utils.http.urlquote().
     the result of django.utils.http.urlquote().
     """
     """
     return force_text(unquote(force_str(quoted_url)))
     return force_text(unquote(force_str(quoted_url)))
-urlunquote = allow_lazy(urlunquote, six.text_type)
 
 
 
 
+@keep_lazy_text
 def urlunquote_plus(quoted_url):
 def urlunquote_plus(quoted_url):
     """
     """
     A wrapper for Python's urllib.unquote_plus() function that can operate on
     A wrapper for Python's urllib.unquote_plus() function that can operate on
     the result of django.utils.http.urlquote_plus().
     the result of django.utils.http.urlquote_plus().
     """
     """
     return force_text(unquote_plus(force_str(quoted_url)))
     return force_text(unquote_plus(force_str(quoted_url)))
-urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type)
 
 
 
 
 def urlencode(query, doseq=0):
 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 import six
 from django.utils.encoding import force_text
 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.safestring import SafeText, mark_safe
 from django.utils.six.moves import html_entities
 from django.utils.six.moves import html_entities
 from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
 from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
@@ -20,7 +20,7 @@ if six.PY2:
 
 
 # Capitalizes the first letter of a string.
 # Capitalizes the first letter of a string.
 capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:]
 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
 # Set up regular expressions
 re_words = re.compile(r'<.*?>|((?:\w[-\w]*|&.*?;)+)', re.U | re.S)
 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]|$)))')
 re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
 
 
 
 
+@keep_lazy_text
 def wrap(text, width):
 def wrap(text, width):
     """
     """
     A word-wrap function that preserves existing line breaks. Expects that
     A word-wrap function that preserves existing line breaks. Expects that
@@ -60,7 +61,6 @@ def wrap(text, width):
             if line:
             if line:
                 yield line
                 yield line
     return ''.join(_generator())
     return ''.join(_generator())
-wrap = allow_lazy(wrap, six.text_type)
 
 
 
 
 class Truncator(SimpleLazyObject):
 class Truncator(SimpleLazyObject):
@@ -95,6 +95,7 @@ class Truncator(SimpleLazyObject):
         string has been truncated, defaulting to a translatable string of an
         string has been truncated, defaulting to a translatable string of an
         ellipsis (...).
         ellipsis (...).
         """
         """
+        self._setup()
         length = int(num)
         length = int(num)
         text = unicodedata.normalize('NFC', self._wrapped)
         text = unicodedata.normalize('NFC', self._wrapped)
 
 
@@ -108,7 +109,6 @@ class Truncator(SimpleLazyObject):
         if html:
         if html:
             return self._truncate_html(length, truncate, text, truncate_len, False)
             return self._truncate_html(length, truncate, text, truncate_len, False)
         return self._text_chars(length, truncate, text, truncate_len)
         return self._text_chars(length, truncate, text, truncate_len)
-    chars = allow_lazy(chars)
 
 
     def _text_chars(self, length, truncate, text, truncate_len):
     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
         argument of what should be used to notify that the string has been
         truncated, defaulting to ellipsis (...).
         truncated, defaulting to ellipsis (...).
         """
         """
+        self._setup()
         length = int(num)
         length = int(num)
         if html:
         if html:
             return self._truncate_html(length, truncate, self._wrapped, length, True)
             return self._truncate_html(length, truncate, self._wrapped, length, True)
         return self._text_words(length, truncate)
         return self._text_words(length, truncate)
-    words = allow_lazy(words)
 
 
     def _text_words(self, length, truncate):
     def _text_words(self, length, truncate):
         """
         """
@@ -229,6 +229,7 @@ class Truncator(SimpleLazyObject):
         return out
         return out
 
 
 
 
+@keep_lazy_text
 def get_valid_filename(s):
 def get_valid_filename(s):
     """
     """
     Returns the given string converted to a string that can be used for a clean
     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(' ', '_')
     s = force_text(s).strip().replace(' ', '_')
     return re.sub(r'(?u)[^-\w.]', '', s)
     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')):
 def get_text_list(list_, last_word=ugettext_lazy('or')):
     """
     """
     >>> get_text_list(['a', 'b', 'c', 'd'])
     >>> 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
         # Translators: This string is used as a separator between list elements
         _(', ').join(force_text(i) for i in list_[:-1]),
         _(', ').join(force_text(i) for i in list_[:-1]),
         force_text(last_word), force_text(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):
 def normalize_newlines(text):
     """Normalizes CRLF and CR newlines to just LF."""
     """Normalizes CRLF and CR newlines to just LF."""
     text = force_text(text)
     text = force_text(text)
     return re_newlines.sub('\n', text)
     return re_newlines.sub('\n', text)
-normalize_newlines = allow_lazy(normalize_newlines, six.text_type)
 
 
 
 
+@keep_lazy_text
 def phone2numeric(phone):
 def phone2numeric(phone):
     """Converts a phone number with letters into its numeric equivalent."""
     """Converts a phone number with letters into its numeric equivalent."""
     char2number = {'a': '2', 'b': '2', 'c': '2', 'd': '3', 'e': '3', 'f': '3',
     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',
          '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'}
          'u': '8', 'v': '8', 'w': '9', 'x': '9', 'y': '9', 'z': '9'}
     return ''.join(char2number.get(c, c) for c in phone.lower())
     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
 # 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}));")
 _entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
 
 
 
 
+@keep_lazy_text
 def unescape_entities(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):
 def unescape_string_literal(s):
     r"""
     r"""
     Convert quoted string literals to unquoted strings with escaped quotes and
     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)
         raise ValueError("Not a string literal: %r" % s)
     quote = s[0]
     quote = s[0]
     return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
     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):
 def slugify(value, allow_unicode=False):
     """
     """
     Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens.
     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 = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
     value = re.sub('[^\w\s-]', '', value).strip().lower()
     value = re.sub('[^\w\s-]', '', value).strip().lower()
     return mark_safe(re.sub('[-\s]+', '-', value))
     return mark_safe(re.sub('[-\s]+', '-', value))
-slugify = allow_lazy(slugify, six.text_type, SafeText)
 
 
 
 
 def camel_case_to_spaces(value):
 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``
 * The ``cascaded_union`` property of ``django.contrib.gis.geos.MultiPolygon``
   will be removed.
   will be removed.
 
 
+* ``django.utils.functional.allow_lazy()`` will be removed.
+
 .. _deprecation-removed-in-1.10:
 .. _deprecation-removed-in-1.10:
 
 
 1.10
 1.10

+ 45 - 9
docs/ref/utils.txt

@@ -522,6 +522,15 @@ Atom1Feed
 
 
 .. function:: allow_lazy(func, *resultclasses)
 .. 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``)
     Django offers many utility functions (particularly in ``django.utils``)
     that take a string as their first argument and do something to that string.
     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
     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
     because you might be using this function outside of a view (and hence the
     current thread's locale setting will not be correct).
     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
     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
     translation as one of its arguments, the function evaluation is delayed
     until it needs to be converted to a string.
     until it needs to be converted to a string.
 
 
     For example::
     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, ...):
         def fancy_utility_function(s, ...):
             # Do some conversion on string '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
     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
     input is a proper string, then add support for lazy translation objects at
     the end.
     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``
 ``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
 * The ``makemigrations --exit`` option is deprecated in favor of the
   :djadminopt:`--check` option.
   :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:
 .. _removed-features-1.10:
 
 
 Features removed in 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
 a ``QuerySet`` can save making an expensive and unnecessary trip to the
 database.
 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
 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
 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
 - 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.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
 from django.middleware.clickjacking import XFrameOptionsMiddleware
 from django.middleware.clickjacking import XFrameOptionsMiddleware
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils import six
 from django.utils.decorators import method_decorator
 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 (
 from django.views.decorators.cache import (
     cache_control, cache_page, never_cache,
     cache_control, cache_page, never_cache,
 )
 )
@@ -67,7 +71,8 @@ full_decorator = compose(
     staff_member_required,
     staff_member_required,
 
 
     # django.utils.functional
     # django.utils.functional
-    allow_lazy,
+    keep_lazy(HttpResponse),
+    keep_lazy_text,
     lazy,
     lazy,
 )
 )
 
 
@@ -149,6 +154,15 @@ class DecoratorsTest(TestCase):
         request.method = 'DELETE'
         request.method = 'DELETE'
         self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed)
         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.
 # 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.
 # 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.test import SimpleTestCase
 from django.utils import six
 from django.utils import six
 from django.utils._os import upath
 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):
 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.template.defaultfilters import escape
 from django.test import SimpleTestCase
 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 django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -33,6 +35,12 @@ class EscapeTests(SimpleTestCase):
         output = self.engine.render_to_string('escape04', {"a": "x&y"})
         output = self.engine.render_to_string('escape04', {"a": "x&y"})
         self.assertEqual(output, "x&amp;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):
 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.template.defaultfilters import escapejs_filter
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 
 
 from ..utils import setup
 from ..utils import setup
 
 
@@ -51,3 +53,11 @@ class FunctionTests(SimpleTestCase):
             escapejs_filter('paragraph separator:\u2029and line separator:\u2028'),
             escapejs_filter('paragraph separator:\u2029and line separator:\u2028'),
             '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.template.defaultfilters import linebreaks_filter
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -51,3 +53,10 @@ class FunctionTests(SimpleTestCase):
             linebreaks_filter('foo\n<a>bar</a>\nbuz', autoescape=False),
             linebreaks_filter('foo\n<a>bar</a>\nbuz', autoescape=False),
             '<p>foo<br /><a>bar</a><br />buz</p>',
             '<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.template.defaultfilters import slugify
 from django.test import SimpleTestCase
 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 django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -41,3 +44,10 @@ class FunctionTests(SimpleTestCase):
 
 
     def test_non_string_input(self):
     def test_non_string_input(self):
         self.assertEqual(slugify(123), '123')
         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.template.defaultfilters import striptags
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils.functional import lazystr
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -40,3 +41,9 @@ class FunctionTests(SimpleTestCase):
 
 
     def test_non_string_input(self):
     def test_non_string_input(self):
         self.assertEqual(striptags(123), '123')
         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.template.defaultfilters import urlize
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils import six
+from django.utils.functional import lazy
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -348,3 +350,10 @@ class FunctionTests(SimpleTestCase):
             urlize('foo<a href=" google.com ">bar</a>buz', autoescape=False),
             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',
             '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.template.defaultfilters import wordwrap
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
+from django.utils.functional import lazystr
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from ..utils import setup
 from ..utils import setup
@@ -41,3 +42,11 @@ class FunctionTests(SimpleTestCase):
 
 
     def test_non_string_input(self):
     def test_non_string_input(self):
         self.assertEqual(wordwrap(123, 2), '123')
         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 import html, safestring, six
 from django.utils._os import upath
 from django.utils._os import upath
 from django.utils.encoding import force_text
 from django.utils.encoding import force_text
+from django.utils.functional import lazystr
 
 
 
 
 class TestUtilsHtml(SimpleTestCase):
 class TestUtilsHtml(SimpleTestCase):
@@ -35,6 +36,7 @@ class TestUtilsHtml(SimpleTestCase):
         for value, output in items:
         for value, output in items:
             for pattern in patterns:
             for pattern in patterns:
                 self.check_output(f, pattern % value, pattern % output)
                 self.check_output(f, pattern % value, pattern % output)
+                self.check_output(f, lazystr(pattern % value), pattern % output)
             # Check repeated values.
             # Check repeated values.
             self.check_output(f, value * 2, output * 2)
             self.check_output(f, value * 2, output * 2)
         # Verify it doesn't double replace &.
         # Verify it doesn't double replace &.
@@ -61,6 +63,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         )
         for value, output in items:
         for value, output in items:
             self.check_output(f, value, output)
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
 
     def test_strip_tags(self):
     def test_strip_tags(self):
         f = html.strip_tags
         f = html.strip_tags
@@ -86,6 +89,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         )
         for value, output in items:
         for value, output in items:
             self.check_output(f, value, output)
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
 
         # Some convoluted syntax for which parsing may differ between python versions
         # Some convoluted syntax for which parsing may differ between python versions
         output = html.strip_tags('<sc<!-- -->ript>test<<!-- -->/script>')
         output = html.strip_tags('<sc<!-- -->ript>test<<!-- -->/script>')
@@ -113,6 +117,7 @@ class TestUtilsHtml(SimpleTestCase):
         items = (' <adf>', '<adf> ', ' </adf> ', ' <f> x</f>')
         items = (' <adf>', '<adf> ', ' </adf> ', ' <f> x</f>')
         for value in items:
         for value in items:
             self.check_output(f, value)
             self.check_output(f, value)
+            self.check_output(f, lazystr(value))
         # Strings that have spaces to strip.
         # Strings that have spaces to strip.
         items = (
         items = (
             ('<d> </d>', '<d></d>'),
             ('<d> </d>', '<d></d>'),
@@ -121,6 +126,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         )
         for value, output in items:
         for value, output in items:
             self.check_output(f, value, output)
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
 
     def test_escapejs(self):
     def test_escapejs(self):
         f = html.escapejs
         f = html.escapejs
@@ -139,6 +145,7 @@ class TestUtilsHtml(SimpleTestCase):
         )
         )
         for value, output in items:
         for value, output in items:
             self.check_output(f, value, output)
             self.check_output(f, value, output)
+            self.check_output(f, lazystr(value), output)
 
 
     def test_smart_urlquote(self):
     def test_smart_urlquote(self):
         quote = html.smart_urlquote
         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.template import Context, Template
 from django.test import SimpleTestCase
 from django.test import SimpleTestCase
 from django.utils import html, six, text
 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 (
 from django.utils.safestring import (
     EscapeData, SafeData, mark_for_escaping, mark_safe,
     EscapeData, SafeData, mark_for_escaping, mark_safe,
 )
 )
 
 
-lazystr = lazy(force_text, six.text_type)
 lazybytes = lazy(force_bytes, bytes)
 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.test import SimpleTestCase
 from django.utils import six, text
 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
 from django.utils.translation import override
 
 
-lazystr = lazy(force_text, six.text_type)
-
 IS_WIDE_BUILD = (len('\U0001F4A9') == 1)
 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
         # Make a best effort to shorten to the desired length, but requesting
         # a length shorter than the ellipsis shouldn't break
         # a length shorter than the ellipsis shouldn't break
         self.assertEqual('...', text.Truncator('asdf').chars(1))
         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):
     def test_truncate_words(self):
         truncator = text.Truncator('The quick brown fox jumped over the lazy '
         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...', truncator.words(4))
         self.assertEqual('The quick brown fox[snip]',
         self.assertEqual('The quick brown fox[snip]',
             truncator.words(4, '[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):
     def test_truncate_html_words(self):
         truncator = text.Truncator('<p id="par"><strong><em>The quick brown fox'
         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(long_word, 20), long_word)
         self.assertEqual(text.wrap('a %s word' % long_word, 10),
         self.assertEqual(text.wrap('a %s word' % long_word, 10),
                          'a\n%s\nword' % long_word)
                          'a\n%s\nword' % long_word)
+        self.assertEqual(text.wrap(lazystr(digits), 100), '1234 67 9')
 
 
     def test_normalize_newlines(self):
     def test_normalize_newlines(self):
         self.assertEqual(text.normalize_newlines("abc\ndef\rghi\r\n"),
         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("\n\r\r\n\r"), "\n\n\n\n")
         self.assertEqual(text.normalize_newlines("abcdefghi"), "abcdefghi")
         self.assertEqual(text.normalize_newlines("abcdefghi"), "abcdefghi")
         self.assertEqual(text.normalize_newlines(""), "")
         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):
     def test_normalize_newlines_bytes(self):
         """normalize_newlines should be able to handle bytes too"""
         """normalize_newlines should be able to handle bytes too"""
@@ -170,6 +174,12 @@ class TestUtilsText(SimpleTestCase):
         self.assertEqual(normalized, "abc\ndef\nghi\n")
         self.assertEqual(normalized, "abc\ndef\nghi\n")
         self.assertIsInstance(normalized, six.text_type)
         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):
     def test_slugify(self):
         items = (
         items = (
             # given - expected - unicode?
             # given - expected - unicode?
@@ -195,10 +205,23 @@ class TestUtilsText(SimpleTestCase):
         ]
         ]
         for value, output in items:
         for value, output in items:
             self.assertEqual(text.unescape_entities(value), output)
             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):
     def test_get_valid_filename(self):
         filename = "^&'@{}[],$=!-#()%+~_123.txt"
         filename = "^&'@{}[],$=!-#()%+~_123.txt"
         self.assertEqual(text.get_valid_filename(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):
     def test_compress_sequence(self):
         data = [{'key': i} for i in range(10)]
         data = [{'key': i} for i in range(10)]