From d693074d431c50e4801dd6bf52525ce1436358f0 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sat, 7 Nov 2015 14:30:20 +0100 Subject: [PATCH] Fixed #20223 -- Added keep_lazy() as a replacement for allow_lazy(). Thanks to bmispelon and uruz for the initial patch. --- AUTHORS | 2 + django/utils/functional.py | 50 +++++++++++++---- django/utils/html.py | 17 +++--- django/utils/http.py | 10 ++-- django/utils/text.py | 26 ++++----- docs/internals/deprecation.txt | 2 + docs/ref/utils.txt | 54 +++++++++++++++---- docs/releases/1.10.txt | 4 ++ docs/topics/performance.txt | 2 +- tests/decorators/tests.py | 18 ++++++- tests/httpwrappers/tests.py | 6 +-- .../filter_tests/test_escape.py | 8 +++ .../filter_tests/test_escapejs.py | 10 ++++ .../filter_tests/test_linebreaks.py | 9 ++++ .../filter_tests/test_slugify.py | 10 ++++ .../filter_tests/test_striptags.py | 7 +++ .../filter_tests/test_urlize.py | 9 ++++ .../filter_tests/test_wordwrap.py | 9 ++++ tests/utils_tests/test_html.py | 7 +++ tests/utils_tests/test_safestring.py | 5 +- tests/utils_tests/test_text.py | 31 +++++++++-- 21 files changed, 237 insertions(+), 59 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5d67d66457..2fb122936b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ answer newbie questions, and generally made Django that much better: Alex Hill Alex Ogier Alex Robbins + Alexey Boriskin Aljosa Mohorovic Amit Chakradeo Amit Ramon @@ -287,6 +288,7 @@ answer newbie questions, and generally made Django that much better: Honza Král Horst Gutmann Hyun Mi Ae + Iacopo Spalletti Ian A Wilson Ian Clelland Ian G. Kelly diff --git a/django/utils/functional.py b/django/utils/functional.py index 7b566e429f..2830cc47ab 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -1,8 +1,10 @@ import copy import operator +import warnings from functools import total_ordering, wraps from django.utils import six +from django.utils.deprecation import RemovedInDjango20Warning # You can't trivially replace this with `functools.partial` because this binds @@ -176,24 +178,52 @@ def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses): return lazy(func, *resultclasses)(*args, **kwargs) +def lazystr(text): + """ + Shortcut for the common case of a lazy callable that returns str. + """ + from django.utils.encoding import force_text # Avoid circular import + return lazy(force_text, six.text_type)(text) + + def allow_lazy(func, *resultclasses): + warnings.warn( + "django.utils.functional.allow_lazy() is deprecated in favor of " + "django.utils.functional.keep_lazy()", + RemovedInDjango20Warning, 2) + return keep_lazy(*resultclasses)(func) + + +def keep_lazy(*resultclasses): """ A decorator that allows a function to be called with one or more lazy arguments. If none of the args are lazy, the function is evaluated immediately, otherwise a __proxy__ is returned that will evaluate the function when needed. """ - lazy_func = lazy(func, *resultclasses) + if not resultclasses: + raise TypeError("You must pass at least one argument to keep_lazy().") - @wraps(func) - def wrapper(*args, **kwargs): - for arg in list(args) + list(kwargs.values()): - if isinstance(arg, Promise): - break - else: - return func(*args, **kwargs) - return lazy_func(*args, **kwargs) - return wrapper + def decorator(func): + lazy_func = lazy(func, *resultclasses) + + @wraps(func) + def wrapper(*args, **kwargs): + for arg in list(args) + list(six.itervalues(kwargs)): + if isinstance(arg, Promise): + break + else: + return func(*args, **kwargs) + return lazy_func(*args, **kwargs) + return wrapper + return decorator + + +def keep_lazy_text(func): + """ + A decorator for functions that accept lazy arguments and return text. + """ + return keep_lazy(six.text_type)(func) empty = object() diff --git a/django/utils/html.py b/django/utils/html.py index 1d86441a28..89d6a00eb2 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -6,7 +6,7 @@ import re from django.utils import six from django.utils.encoding import force_str, force_text -from django.utils.functional import allow_lazy +from django.utils.functional import keep_lazy, keep_lazy_text from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS from django.utils.safestring import SafeData, SafeText, mark_safe from django.utils.six.moves.urllib.parse import ( @@ -38,6 +38,7 @@ hard_coded_bullets_re = re.compile( trailing_empty_content_re = re.compile(r'(?:

(?: |\s|
)*?

\s*)+\Z') +@keep_lazy(six.text_type, SafeText) def escape(text): """ Returns the given text with ampersands, quotes and angle brackets encoded @@ -49,7 +50,6 @@ def escape(text): """ return mark_safe(force_text(text).replace('&', '&').replace('<', '<') .replace('>', '>').replace('"', '"').replace("'", ''')) -escape = allow_lazy(escape, six.text_type, SafeText) _js_escapes = { ord('\\'): '\\u005C', @@ -69,10 +69,10 @@ _js_escapes = { _js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32)) +@keep_lazy(six.text_type, SafeText) def escapejs(value): """Hex encodes characters for use in JavaScript strings.""" return mark_safe(force_text(value).translate(_js_escapes)) -escapejs = allow_lazy(escapejs, six.text_type, SafeText) def conditional_escape(text): @@ -118,16 +118,16 @@ def format_html_join(sep, format_string, args_generator): for args in args_generator)) +@keep_lazy_text def linebreaks(value, autoescape=False): """Converts newlines into

and
s.""" - value = normalize_newlines(value) + value = normalize_newlines(force_text(value)) paras = re.split('\n{2,}', value) if autoescape: paras = ['

%s

' % escape(p).replace('\n', '
') for p in paras] else: paras = ['

%s

' % p.replace('\n', '
') for p in paras] return '\n\n'.join(paras) -linebreaks = allow_lazy(linebreaks, six.text_type) class MLStripper(HTMLParser): @@ -166,10 +166,12 @@ def _strip_once(value): return s.get_data() +@keep_lazy_text def strip_tags(value): """Returns the given HTML with all tags stripped.""" # Note: in typical case this loop executes _strip_once once. Loop condition # is redundant, but helps to reduce number of executions of _strip_once. + value = force_text(value) while '<' in value and '>' in value: new_value = _strip_once(value) if len(new_value) >= len(value): @@ -179,13 +181,12 @@ def strip_tags(value): break value = new_value return value -strip_tags = allow_lazy(strip_tags) +@keep_lazy_text def strip_spaces_between_tags(value): """Returns the given HTML with spaces between tags removed.""" return re.sub(r'>\s+<', '><', force_text(value)) -strip_spaces_between_tags = allow_lazy(strip_spaces_between_tags, six.text_type) def smart_urlquote(url): @@ -224,6 +225,7 @@ def smart_urlquote(url): return urlunsplit((scheme, netloc, path, query, fragment)) +@keep_lazy_text def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): """ Converts any URLs in text into clickable links. @@ -321,7 +323,6 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): elif autoescape: words[i] = escape(word) return ''.join(words) -urlize = allow_lazy(urlize, six.text_type) def avoid_wrapping(value): diff --git a/django/utils/http.py b/django/utils/http.py index 8bbafaedec..3a3f665443 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -12,7 +12,7 @@ from email.utils import formatdate from django.utils import six from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_bytes, force_str, force_text -from django.utils.functional import allow_lazy +from django.utils.functional import keep_lazy_text from django.utils.six.moves.urllib.parse import ( quote, quote_plus, unquote, unquote_plus, urlencode as original_urlencode, urlparse, @@ -40,6 +40,7 @@ PROTOCOL_TO_PORT = { } +@keep_lazy_text def urlquote(url, safe='/'): """ A version of Python's urllib.quote() function that can operate on unicode @@ -48,9 +49,9 @@ def urlquote(url, safe='/'): without double-quoting occurring. """ return force_text(quote(force_str(url), force_str(safe))) -urlquote = allow_lazy(urlquote, six.text_type) +@keep_lazy_text def urlquote_plus(url, safe=''): """ A version of Python's urllib.quote_plus() function that can operate on @@ -59,25 +60,24 @@ def urlquote_plus(url, safe=''): iri_to_uri() call without double-quoting occurring. """ return force_text(quote_plus(force_str(url), force_str(safe))) -urlquote_plus = allow_lazy(urlquote_plus, six.text_type) +@keep_lazy_text def urlunquote(quoted_url): """ A wrapper for Python's urllib.unquote() function that can operate on the result of django.utils.http.urlquote(). """ return force_text(unquote(force_str(quoted_url))) -urlunquote = allow_lazy(urlunquote, six.text_type) +@keep_lazy_text def urlunquote_plus(quoted_url): """ A wrapper for Python's urllib.unquote_plus() function that can operate on the result of django.utils.http.urlquote_plus(). """ return force_text(unquote_plus(force_str(quoted_url))) -urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type) def urlencode(query, doseq=0): diff --git a/django/utils/text.py b/django/utils/text.py index 456c853712..af80812a0d 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -7,7 +7,7 @@ from io import BytesIO from django.utils import six from django.utils.encoding import force_text -from django.utils.functional import SimpleLazyObject, allow_lazy +from django.utils.functional import SimpleLazyObject, keep_lazy, keep_lazy_text from django.utils.safestring import SafeText, mark_safe from django.utils.six.moves import html_entities from django.utils.translation import pgettext, ugettext as _, ugettext_lazy @@ -20,7 +20,7 @@ if six.PY2: # Capitalizes the first letter of a string. capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:] -capfirst = allow_lazy(capfirst, six.text_type) +capfirst = keep_lazy_text(capfirst) # Set up regular expressions re_words = re.compile(r'<.*?>|((?:\w[-\w]*|&.*?;)+)', re.U | re.S) @@ -30,6 +30,7 @@ re_newlines = re.compile(r'\r\n|\r') # Used in normalize_newlines re_camel_case = re.compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') +@keep_lazy_text def wrap(text, width): """ A word-wrap function that preserves existing line breaks. Expects that @@ -60,7 +61,6 @@ def wrap(text, width): if line: yield line return ''.join(_generator()) -wrap = allow_lazy(wrap, six.text_type) class Truncator(SimpleLazyObject): @@ -95,6 +95,7 @@ class Truncator(SimpleLazyObject): string has been truncated, defaulting to a translatable string of an ellipsis (...). """ + self._setup() length = int(num) text = unicodedata.normalize('NFC', self._wrapped) @@ -108,7 +109,6 @@ class Truncator(SimpleLazyObject): if html: return self._truncate_html(length, truncate, text, truncate_len, False) return self._text_chars(length, truncate, text, truncate_len) - chars = allow_lazy(chars) def _text_chars(self, length, truncate, text, truncate_len): """ @@ -138,11 +138,11 @@ class Truncator(SimpleLazyObject): argument of what should be used to notify that the string has been truncated, defaulting to ellipsis (...). """ + self._setup() length = int(num) if html: return self._truncate_html(length, truncate, self._wrapped, length, True) return self._text_words(length, truncate) - words = allow_lazy(words) def _text_words(self, length, truncate): """ @@ -229,6 +229,7 @@ class Truncator(SimpleLazyObject): return out +@keep_lazy_text def get_valid_filename(s): """ Returns the given string converted to a string that can be used for a clean @@ -240,9 +241,9 @@ def get_valid_filename(s): """ s = force_text(s).strip().replace(' ', '_') return re.sub(r'(?u)[^-\w.]', '', s) -get_valid_filename = allow_lazy(get_valid_filename, six.text_type) +@keep_lazy_text def get_text_list(list_, last_word=ugettext_lazy('or')): """ >>> get_text_list(['a', 'b', 'c', 'd']) @@ -264,16 +265,16 @@ def get_text_list(list_, last_word=ugettext_lazy('or')): # Translators: This string is used as a separator between list elements _(', ').join(force_text(i) for i in list_[:-1]), force_text(last_word), force_text(list_[-1])) -get_text_list = allow_lazy(get_text_list, six.text_type) +@keep_lazy_text def normalize_newlines(text): """Normalizes CRLF and CR newlines to just LF.""" text = force_text(text) return re_newlines.sub('\n', text) -normalize_newlines = allow_lazy(normalize_newlines, six.text_type) +@keep_lazy_text def phone2numeric(phone): """Converts a phone number with letters into its numeric equivalent.""" char2number = {'a': '2', 'b': '2', 'c': '2', 'd': '3', 'e': '3', 'f': '3', @@ -281,7 +282,6 @@ def phone2numeric(phone): 'n': '6', 'o': '6', 'p': '7', 'q': '7', 'r': '7', 's': '7', 't': '8', 'u': '8', 'v': '8', 'w': '9', 'x': '9', 'y': '9', 'z': '9'} return ''.join(char2number.get(c, c) for c in phone.lower()) -phone2numeric = allow_lazy(phone2numeric) # From http://www.xhaus.com/alan/python/httpcomp.html#gzip @@ -384,11 +384,12 @@ def _replace_entity(match): _entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));") +@keep_lazy_text def unescape_entities(text): - return _entity_re.sub(_replace_entity, text) -unescape_entities = allow_lazy(unescape_entities, six.text_type) + return _entity_re.sub(_replace_entity, force_text(text)) +@keep_lazy_text def unescape_string_literal(s): r""" Convert quoted string literals to unquoted strings with escaped quotes and @@ -407,9 +408,9 @@ def unescape_string_literal(s): raise ValueError("Not a string literal: %r" % s) quote = s[0] return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\') -unescape_string_literal = allow_lazy(unescape_string_literal) +@keep_lazy(six.text_type, SafeText) def slugify(value, allow_unicode=False): """ Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens. @@ -424,7 +425,6 @@ def slugify(value, allow_unicode=False): value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') value = re.sub('[^\w\s-]', '', value).strip().lower() return mark_safe(re.sub('[-\s]+', '-', value)) -slugify = allow_lazy(slugify, six.text_type, SafeText) def camel_case_to_spaces(value): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 6a312dc925..5fe2e142fb 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -124,6 +124,8 @@ details on these changes. * The ``cascaded_union`` property of ``django.contrib.gis.geos.MultiPolygon`` will be removed. +* ``django.utils.functional.allow_lazy()`` will be removed. + .. _deprecation-removed-in-1.10: 1.10 diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 0a7dfd43de..369ec6eb51 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -522,6 +522,15 @@ Atom1Feed .. function:: allow_lazy(func, *resultclasses) + .. deprecated:: 1.10 + + Works like :meth:`~django.utils.functional.keep_lazy` except that it can't + be used as a decorator. + +.. function:: keep_lazy(func, *resultclasses) + + .. versionadded:: 1.10 + Django offers many utility functions (particularly in ``django.utils``) that take a string as their first argument and do something to that string. These functions are used by template filters as well as directly in other @@ -533,31 +542,58 @@ Atom1Feed because you might be using this function outside of a view (and hence the current thread's locale setting will not be correct). - For cases like this, use the ``django.utils.functional.allow_lazy()`` + For cases like this, use the ``django.utils.functional.keep_lazy()`` decorator. It modifies the function so that *if* it's called with a lazy translation as one of its arguments, the function evaluation is delayed until it needs to be converted to a string. For example:: - from django.utils.functional import allow_lazy + from django.utils import six + from django.utils.functional import keep_lazy, keep_lazy_text def fancy_utility_function(s, ...): # Do some conversion on string 's' ... - # Replace unicode by str on Python 3 - fancy_utility_function = allow_lazy(fancy_utility_function, unicode) + fancy_utility_function = keep_lazy(six.text_type)(fancy_utility_function) - The ``allow_lazy()`` decorator takes, in addition to the function to - decorate, a number of extra arguments (``*args``) specifying the type(s) - that the original function can return. Usually, it's enough to include - ``unicode`` (or ``str`` on Python 3) here and ensure that your function - returns only Unicode strings. + # Or more succinctly: + @keep_lazy(six.text_type) + def fancy_utility_function(s, ...): + ... + + The ``keep_lazy()`` decorator takes a number of extra arguments (``*args``) + specifying the type(s) that the original function can return. A common + use case is to have functions that return text. For these, you can just + pass the ``six.text_type`` type to ``keep_lazy`` (or even simpler, use the + :func:`keep_lazy_text` decorator described in the next section). Using this decorator means you can write your function and assume that the input is a proper string, then add support for lazy translation objects at the end. +.. function:: keep_lazy_text(func) + + .. versionadded:: 1.10 + + A shortcut for ``keep_lazy(six.text_type)(func)``. + + If you have a function that returns text and you want to be able to take + lazy arguments while delaying their evaluation, simply use this decorator:: + + from django.utils import six + from django.utils.functional import keep_lazy, keep_lazy_text + + # Our previous example was: + @keep_lazy(six.text_type) + def fancy_utility_function(s, ...): + ... + + # Which can be rewritten as: + @keep_lazy_text + def fancy_utility_function(s, ...): + ... + ``django.utils.html`` ===================== diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index f8e90bbed7..6f94154929 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -402,6 +402,10 @@ Miscellaneous * The ``makemigrations --exit`` option is deprecated in favor of the :djadminopt:`--check` option. +* ``django.utils.functional.allow_lazy()`` is deprecated in favor of the new + :func:`~django.utils.functional.keep_lazy` function which can be used with a + more natural decorator syntax. + .. _removed-features-1.10: Features removed in 1.10 diff --git a/docs/topics/performance.txt b/docs/topics/performance.txt index 718fa4762e..6725a624eb 100644 --- a/docs/topics/performance.txt +++ b/docs/topics/performance.txt @@ -223,7 +223,7 @@ QuerySet `. Avoiding the premature evaluation of a ``QuerySet`` can save making an expensive and unnecessary trip to the database. -Django also offers an :meth:`~django.utils.functional.allow_lazy` decorator. +Django also offers a :meth:`~django.utils.functional.keep_lazy` decorator. This allows a function that has been called with a lazy argument to behave lazily itself, only being evaluated when it needs to be. Thus the lazy argument - which could be an expensive one - will not be called upon for evaluation diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index d23d700201..8fed78a67f 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -8,8 +8,12 @@ from django.contrib.auth.decorators import ( from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed from django.middleware.clickjacking import XFrameOptionsMiddleware from django.test import SimpleTestCase +from django.utils import six from django.utils.decorators import method_decorator -from django.utils.functional import allow_lazy, lazy +from django.utils.deprecation import RemovedInDjango20Warning +from django.utils.encoding import force_text +from django.utils.functional import allow_lazy, keep_lazy, keep_lazy_text, lazy +from django.utils.translation import ugettext_lazy from django.views.decorators.cache import ( cache_control, cache_page, never_cache, ) @@ -67,7 +71,8 @@ full_decorator = compose( staff_member_required, # django.utils.functional - allow_lazy, + keep_lazy(HttpResponse), + keep_lazy_text, lazy, ) @@ -149,6 +154,15 @@ class DecoratorsTest(TestCase): request.method = 'DELETE' self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) + def test_deprecated_allow_lazy(self): + with self.assertRaises(RemovedInDjango20Warning): + def noop_text(text): + return force_text(text) + noop_text = allow_lazy(noop_text, six.text_type) + rendered = noop_text(ugettext_lazy("I am a text")) + self.assertEqual(type(rendered), six.text_type) + self.assertEqual(rendered, "I am a text") + # For testing method_decorator, a decorator that assumes a single argument. # We will get type arguments if there is a mismatch in the number of arguments. diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 9edc7d12af..94d269c90b 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -21,10 +21,8 @@ from django.http import ( from django.test import SimpleTestCase from django.utils import six from django.utils._os import upath -from django.utils.encoding import force_text, smart_str -from django.utils.functional import lazy - -lazystr = lazy(force_text, six.text_type) +from django.utils.encoding import smart_str +from django.utils.functional import lazystr class QueryDictTests(unittest.TestCase): diff --git a/tests/template_tests/filter_tests/test_escape.py b/tests/template_tests/filter_tests/test_escape.py index 83bfb36444..7dba5e1637 100644 --- a/tests/template_tests/filter_tests/test_escape.py +++ b/tests/template_tests/filter_tests/test_escape.py @@ -1,5 +1,7 @@ from django.template.defaultfilters import escape from django.test import SimpleTestCase +from django.utils import six +from django.utils.functional import Promise, lazy from django.utils.safestring import mark_safe from ..utils import setup @@ -33,6 +35,12 @@ class EscapeTests(SimpleTestCase): output = self.engine.render_to_string('escape04', {"a": "x&y"}) self.assertEqual(output, "x&y") + def test_escape_lazy_string(self): + add_html = lazy(lambda string: string + 'special characters > here', six.text_type) + escaped = escape(add_html('this' + 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' + ) diff --git a/tests/template_tests/filter_tests/test_linebreaks.py b/tests/template_tests/filter_tests/test_linebreaks.py index d895a067bb..144facf399 100644 --- a/tests/template_tests/filter_tests/test_linebreaks.py +++ b/tests/template_tests/filter_tests/test_linebreaks.py @@ -1,5 +1,7 @@ from django.template.defaultfilters import linebreaks_filter from django.test import SimpleTestCase +from django.utils import six +from django.utils.functional import lazy from django.utils.safestring import mark_safe from ..utils import setup @@ -51,3 +53,10 @@ class FunctionTests(SimpleTestCase): linebreaks_filter('foo\nbar\nbuz', autoescape=False), '

foo
bar
buz

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

Header

\n\n

line 1
line2

' + ) diff --git a/tests/template_tests/filter_tests/test_slugify.py b/tests/template_tests/filter_tests/test_slugify.py index 1ab5b947b4..5852417b2e 100644 --- a/tests/template_tests/filter_tests/test_slugify.py +++ b/tests/template_tests/filter_tests/test_slugify.py @@ -3,6 +3,9 @@ from __future__ import unicode_literals from django.template.defaultfilters import slugify from django.test import SimpleTestCase +from django.utils import six +from django.utils.encoding import force_text +from django.utils.functional import lazy from django.utils.safestring import mark_safe from ..utils import setup @@ -41,3 +44,10 @@ class FunctionTests(SimpleTestCase): def test_non_string_input(self): self.assertEqual(slugify(123), '123') + + def test_slugify_lazy_string(self): + lazy_str = lazy(lambda string: force_text(string), six.text_type) + self.assertEqual( + slugify(lazy_str(' Jack & Jill like numbers 1,2,3 and 4 and silly characters ?%.$!/')), + 'jack-jill-like-numbers-123-and-4-and-silly-characters', + ) diff --git a/tests/template_tests/filter_tests/test_striptags.py b/tests/template_tests/filter_tests/test_striptags.py index 27f5b393bb..8b946d1f83 100644 --- a/tests/template_tests/filter_tests/test_striptags.py +++ b/tests/template_tests/filter_tests/test_striptags.py @@ -1,5 +1,6 @@ from django.template.defaultfilters import striptags from django.test import SimpleTestCase +from django.utils.functional import lazystr from django.utils.safestring import mark_safe from ..utils import setup @@ -40,3 +41,9 @@ class FunctionTests(SimpleTestCase): def test_non_string_input(self): self.assertEqual(striptags(123), '123') + + def test_strip_lazy_string(self): + self.assertEqual( + striptags(lazystr('some html with disallowed tags')), + 'some html with alert("Hello") disallowed tags', + ) diff --git a/tests/template_tests/filter_tests/test_urlize.py b/tests/template_tests/filter_tests/test_urlize.py index 81451f2d4e..9cf3f982a8 100644 --- a/tests/template_tests/filter_tests/test_urlize.py +++ b/tests/template_tests/filter_tests/test_urlize.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals from django.template.defaultfilters import urlize from django.test import SimpleTestCase +from django.utils import six +from django.utils.functional import lazy from django.utils.safestring import mark_safe from ..utils import setup @@ -348,3 +350,10 @@ class FunctionTests(SimpleTestCase): urlize('foobarbuz', autoescape=False), 'foogoogle.com ">barbuz', ) + + def test_lazystring(self): + prepend_www = lazy(lambda url: 'www.' + url, six.text_type) + self.assertEqual( + urlize(prepend_www('google.com')), + 'www.google.com', + ) diff --git a/tests/template_tests/filter_tests/test_wordwrap.py b/tests/template_tests/filter_tests/test_wordwrap.py index dc8ab10868..b844ec5108 100644 --- a/tests/template_tests/filter_tests/test_wordwrap.py +++ b/tests/template_tests/filter_tests/test_wordwrap.py @@ -1,5 +1,6 @@ from django.template.defaultfilters import wordwrap from django.test import SimpleTestCase +from django.utils.functional import lazystr from django.utils.safestring import mark_safe from ..utils import setup @@ -41,3 +42,11 @@ class FunctionTests(SimpleTestCase): def test_non_string_input(self): self.assertEqual(wordwrap(123, 2), '123') + + def test_wrap_lazy_string(self): + self.assertEqual( + wordwrap(lazystr( + 'this is a long paragraph of text that really needs to be wrapped I\'m afraid' + ), 14), + 'this is a long\nparagraph of\ntext that\nreally needs\nto be wrapped\nI\'m afraid', + ) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index ed026f0a28..20de85148e 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -8,6 +8,7 @@ from django.test import SimpleTestCase from django.utils import html, safestring, six from django.utils._os import upath from django.utils.encoding import force_text +from django.utils.functional import lazystr class TestUtilsHtml(SimpleTestCase): @@ -35,6 +36,7 @@ class TestUtilsHtml(SimpleTestCase): for value, output in items: for pattern in patterns: self.check_output(f, pattern % value, pattern % output) + self.check_output(f, lazystr(pattern % value), pattern % output) # Check repeated values. self.check_output(f, value * 2, output * 2) # Verify it doesn't double replace &. @@ -61,6 +63,7 @@ class TestUtilsHtml(SimpleTestCase): ) for value, output in items: self.check_output(f, value, output) + self.check_output(f, lazystr(value), output) def test_strip_tags(self): f = html.strip_tags @@ -86,6 +89,7 @@ class TestUtilsHtml(SimpleTestCase): ) for value, output in items: self.check_output(f, value, output) + self.check_output(f, lazystr(value), output) # Some convoluted syntax for which parsing may differ between python versions output = html.strip_tags('ript>test</script>') @@ -113,6 +117,7 @@ class TestUtilsHtml(SimpleTestCase): items = (' ', ' ', ' ', ' x') for value in items: self.check_output(f, value) + self.check_output(f, lazystr(value)) # Strings that have spaces to strip. items = ( (' ', ''), @@ -121,6 +126,7 @@ class TestUtilsHtml(SimpleTestCase): ) for value, output in items: self.check_output(f, value, output) + self.check_output(f, lazystr(value), output) def test_escapejs(self): f = html.escapejs @@ -139,6 +145,7 @@ class TestUtilsHtml(SimpleTestCase): ) for value, output in items: self.check_output(f, value, output) + self.check_output(f, lazystr(value), output) def test_smart_urlquote(self): quote = html.smart_urlquote diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py index acd36053d6..7cc92a1370 100644 --- a/tests/utils_tests/test_safestring.py +++ b/tests/utils_tests/test_safestring.py @@ -3,13 +3,12 @@ from __future__ import unicode_literals from django.template import Context, Template from django.test import SimpleTestCase from django.utils import html, six, text -from django.utils.encoding import force_bytes, force_text -from django.utils.functional import lazy +from django.utils.encoding import force_bytes +from django.utils.functional import lazy, lazystr from django.utils.safestring import ( EscapeData, SafeData, mark_for_escaping, mark_safe, ) -lazystr = lazy(force_text, six.text_type) lazybytes = lazy(force_bytes, bytes) diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py index e6f5c0f378..5d97844800 100644 --- a/tests/utils_tests/test_text.py +++ b/tests/utils_tests/test_text.py @@ -5,12 +5,9 @@ import json from django.test import SimpleTestCase from django.utils import six, text -from django.utils.encoding import force_text -from django.utils.functional import lazy +from django.utils.functional import lazystr from django.utils.translation import override -lazystr = lazy(force_text, six.text_type) - IS_WIDE_BUILD = (len('\U0001F4A9') == 1) @@ -93,6 +90,8 @@ class TestUtilsText(SimpleTestCase): # Make a best effort to shorten to the desired length, but requesting # a length shorter than the ellipsis shouldn't break self.assertEqual('...', text.Truncator('asdf').chars(1)) + # Ensure that lazy strings are handled correctly + self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(12), 'The quick...') def test_truncate_words(self): truncator = text.Truncator('The quick brown fox jumped over the lazy ' @@ -102,6 +101,9 @@ class TestUtilsText(SimpleTestCase): self.assertEqual('The quick brown fox...', truncator.words(4)) self.assertEqual('The quick brown fox[snip]', truncator.words(4, '[snip]')) + # Ensure that lazy strings are handled correctly + truncator = text.Truncator(lazystr('The quick brown fox jumped over the lazy dog.')) + self.assertEqual('The quick brown fox...', truncator.words(4)) def test_truncate_html_words(self): truncator = text.Truncator('

The quick brown fox' @@ -156,6 +158,7 @@ class TestUtilsText(SimpleTestCase): self.assertEqual(text.wrap(long_word, 20), long_word) self.assertEqual(text.wrap('a %s word' % long_word, 10), 'a\n%s\nword' % long_word) + self.assertEqual(text.wrap(lazystr(digits), 100), '1234 67 9') def test_normalize_newlines(self): self.assertEqual(text.normalize_newlines("abc\ndef\rghi\r\n"), @@ -163,6 +166,7 @@ class TestUtilsText(SimpleTestCase): self.assertEqual(text.normalize_newlines("\n\r\r\n\r"), "\n\n\n\n") self.assertEqual(text.normalize_newlines("abcdefghi"), "abcdefghi") self.assertEqual(text.normalize_newlines(""), "") + self.assertEqual(text.normalize_newlines(lazystr("abc\ndef\rghi\r\n")), "abc\ndef\nghi\n") def test_normalize_newlines_bytes(self): """normalize_newlines should be able to handle bytes too""" @@ -170,6 +174,12 @@ class TestUtilsText(SimpleTestCase): self.assertEqual(normalized, "abc\ndef\nghi\n") self.assertIsInstance(normalized, six.text_type) + def test_phone2numeric(self): + numeric = text.phone2numeric('0800 flowers') + self.assertEqual(numeric, '0800 3569377') + lazy_numeric = lazystr(text.phone2numeric('0800 flowers')) + self.assertEqual(lazy_numeric, '0800 3569377') + def test_slugify(self): items = ( # given - expected - unicode? @@ -195,10 +205,23 @@ class TestUtilsText(SimpleTestCase): ] for value, output in items: self.assertEqual(text.unescape_entities(value), output) + self.assertEqual(text.unescape_entities(lazystr(value)), output) + + def test_unescape_string_literal(self): + items = [ + ('"abc"', 'abc'), + ("'abc'", 'abc'), + ('"a \"bc\""', 'a "bc"'), + ("'\'ab\' c'", "'ab' c"), + ] + for value, output in items: + self.assertEqual(text.unescape_string_literal(value), output) + self.assertEqual(text.unescape_string_literal(lazystr(value)), output) def test_get_valid_filename(self): filename = "^&'@{}[],$=!-#()%+~_123.txt" self.assertEqual(text.get_valid_filename(filename), "-_123.txt") + self.assertEqual(text.get_valid_filename(lazystr(filename)), "-_123.txt") def test_compress_sequence(self): data = [{'key': i} for i in range(10)]