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

Thanks to bmispelon and uruz for the initial patch.
This commit is contained in:
Iacopo Spalletti 2015-11-07 14:30:20 +01:00 committed by Tim Graham
parent 93fc23b2d5
commit d693074d43
21 changed files with 237 additions and 59 deletions

View File

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

View File

@ -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.
""" """
if not resultclasses:
raise TypeError("You must pass at least one argument to keep_lazy().")
def decorator(func):
lazy_func = lazy(func, *resultclasses) lazy_func = lazy(func, *resultclasses)
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
for arg in list(args) + list(kwargs.values()): for arg in list(args) + list(six.itervalues(kwargs)):
if isinstance(arg, Promise): if isinstance(arg, Promise):
break break
else: else:
return func(*args, **kwargs) return func(*args, **kwargs)
return lazy_func(*args, **kwargs) return lazy_func(*args, **kwargs)
return wrapper 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()

View File

@ -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):

View File

@ -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):

View File

@ -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) return _entity_re.sub(_replace_entity, force_text(text))
unescape_entities = allow_lazy(unescape_entities, six.text_type)
@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):

View File

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

View File

@ -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 = keep_lazy(six.text_type)(fancy_utility_function)
fancy_utility_function = allow_lazy(fancy_utility_function, unicode)
The ``allow_lazy()`` decorator takes, in addition to the function to # Or more succinctly:
decorate, a number of extra arguments (``*args``) specifying the type(s) @keep_lazy(six.text_type)
that the original function can return. Usually, it's enough to include def fancy_utility_function(s, ...):
``unicode`` (or ``str`` on Python 3) here and ensure that your function ...
returns only Unicode strings.
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``
===================== =====================

View File

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

View File

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

View File

@ -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.

View File

@ -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.encoding import smart_str
from django.utils.functional import lazy from django.utils.functional import lazystr
lazystr = lazy(force_text, six.text_type)
class QueryDictTests(unittest.TestCase): class QueryDictTests(unittest.TestCase):

View File

@ -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):

View File

@ -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'
)

View File

@ -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>'
)

View File

@ -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',
)

View File

@ -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',
)

View File

@ -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>',
)

View File

@ -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',
)

View File

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

View File

@ -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.encoding import force_bytes
from django.utils.functional import lazy 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)

View File

@ -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 lazystr
from django.utils.functional import lazy
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)]