diff --git a/django/template/base.py b/django/template/base.py index c48746bbef..d97ee81326 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -54,6 +54,7 @@ from __future__ import unicode_literals import inspect import logging import re +import warnings from django.template.context import ( # NOQA: imported for backwards compatibility BaseContext, Context, ContextPopException, RequestContext, @@ -722,6 +723,7 @@ class FilterExpression(object): obj = string_if_invalid else: obj = self.var + escape_isnt_last_filter = True for func, args in self.filters: arg_vals = [] for lookup, arg in args: @@ -738,9 +740,21 @@ class FilterExpression(object): if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): obj = mark_safe(new_obj) elif isinstance(obj, EscapeData): - obj = mark_for_escaping(new_obj) + with warnings.catch_warnings(): + # Ignore mark_for_escaping deprecation as this will be + # removed in Django 2.0. + warnings.simplefilter('ignore', category=RemovedInDjango20Warning) + obj = mark_for_escaping(new_obj) + escape_isnt_last_filter = False else: obj = new_obj + if not escape_isnt_last_filter: + warnings.warn( + "escape isn't the last filter in %s and will be applied " + "immediately in Django 2.0 so the output may change." + % [func.__name__ for func, _ in self.filters], + RemovedInDjango20Warning, stacklevel=2 + ) return obj def args_check(name, func, provided): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index a4d568e5a6..e3dc48e474 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import random as random_module import re +import warnings from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation from functools import wraps from operator import itemgetter @@ -10,6 +11,7 @@ from pprint import pformat from django.utils import formats, six from django.utils.dateformat import format, time_format +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_text, iri_to_uri from django.utils.html import ( avoid_wrapping, conditional_escape, escape, escapejs, linebreaks, @@ -439,7 +441,11 @@ def escape_filter(value): """ Marks the value as a string that should be auto-escaped. """ - return mark_for_escaping(value) + with warnings.catch_warnings(): + # Ignore mark_for_escaping deprecation -- this will use + # conditional_escape() in Django 2.0. + warnings.simplefilter('ignore', category=RemovedInDjango20Warning) + return mark_for_escaping(value) @register.filter(is_safe=True) diff --git a/django/utils/safestring.py b/django/utils/safestring.py index 3d3bf1b62a..24a29e0747 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -4,7 +4,10 @@ without further escaping in HTML. Marking something as a "safe string" means that the producer of the string has already turned characters that should not be interpreted by the HTML engine (e.g. '<') into the appropriate entities. """ +import warnings + from django.utils import six +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.functional import Promise, curry @@ -138,6 +141,7 @@ def mark_for_escaping(s): Can be called multiple times on a single string (the resulting escaping is only applied once). """ + warnings.warn('mark_for_escaping() is deprecated.', RemovedInDjango20Warning) if hasattr(s, '__html__') or isinstance(s, EscapeData): return s if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 97179de7b0..69e223c01c 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -210,15 +210,6 @@ passed around inside the template code: # Do something with the "safe" string. ... -* **Strings marked as "needing escaping"** are *always* escaped on - output, regardless of whether they are in an :ttag:`autoescape` block or - not. These strings are only escaped once, however, even if auto-escaping - applies. - - Internally, these strings are of type ``EscapeBytes`` or - ``EscapeText``. Generally you don't have to worry about these; they - exist for the implementation of the :tfilter:`escape` filter. - Template filter code falls into one of two situations: 1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``, diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b3d03dd37a..218e79d66f 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -168,6 +168,13 @@ details on these changes. * ``FileField`` methods ``get_directory_name()`` and ``get_filename()`` will be removed. +* The ``mark_for_escaping()`` function and the classes it uses: ``EscapeData``, + ``EscapeBytes``, ``EscapeText``, ``EscapeString``, and ``EscapeUnicode`` will + be removed. + +* The ``escape`` filter will change to use + ``django.utils.html.conditional_escape()``. + .. _deprecation-removed-in-1.10: 1.10 diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 0959679ed7..f44dbb0a6f 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -1578,6 +1578,12 @@ For example, you can apply ``escape`` to fields when :ttag:`autoescape` is off:: {{ title|escape }} {% endautoescape %} +.. deprecated:: 1.10 + + The "lazy" behavior of the ``escape`` filter is deprecated. It will change + to immediately apply :func:`~django.utils.html.conditional_escape` in + Django 2.0. + .. templatefilter:: escapejs ``escapejs`` diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 8328d91fc5..eadd036cb6 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -839,6 +839,8 @@ appropriate entities. .. function:: mark_for_escaping(s) + .. deprecated:: 1.10 + Explicitly mark a string as requiring HTML escaping upon output. Has no effect on ``SafeData`` subclasses. diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index a0b0a873cc..410180f838 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -1012,6 +1012,18 @@ This method must accept a :class:`~django.db.models.query.QuerySet` instance as its single argument and return a filtered version of the queryset for the model instance the manager is bound to. +The "escape" half of ``django.utils.safestring`` +------------------------------------------------ + +The ``mark_for_escaping()`` function and the classes it uses: ``EscapeData``, +``EscapeBytes``, ``EscapeText``, ``EscapeString``, and ``EscapeUnicode`` are +deprecated. + +As a result, the "lazy" behavior of the ``escape`` filter (where it would +always be applied as the last filter no matter where in the filter chain it +appeared) is deprecated. The filter will change to immediately apply +:func:`~django.utils.html.conditional_escape` in Django 2.0. + Miscellaneous ------------- diff --git a/docs/releases/1.7.2.txt b/docs/releases/1.7.2.txt index 040c983fcb..056f432978 100644 --- a/docs/releases/1.7.2.txt +++ b/docs/releases/1.7.2.txt @@ -177,7 +177,7 @@ Bugfixes setup (:ticket:`24000`). * Restored support for objects that aren't :class:`str` or :class:`bytes` in - :func:`~django.utils.safestring.mark_for_escaping` on Python 3. + ``django.utils.safestring.mark_for_escaping()`` on Python 3. * Supported strings escaped by third-party libraries with the ``__html__`` convention in the template engine (:ticket:`23831`). diff --git a/tests/template_tests/filter_tests/test_chaining.py b/tests/template_tests/filter_tests/test_chaining.py index 9bc3976f37..453e2a335f 100644 --- a/tests/template_tests/filter_tests/test_chaining.py +++ b/tests/template_tests/filter_tests/test_chaining.py @@ -1,4 +1,7 @@ -from django.test import SimpleTestCase +import warnings + +from django.test import SimpleTestCase, ignore_warnings +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.safestring import mark_safe from ..utils import setup @@ -38,9 +41,19 @@ class ChainingTests(SimpleTestCase): # Using a filter that forces safeness does not lead to double-escaping @setup({'chaining05': '{{ a|escape|capfirst }}'}) def test_chaining05(self): - output = self.engine.render_to_string('chaining05', {'a': 'a < b'}) - self.assertEqual(output, 'A < b') + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + output = self.engine.render_to_string('chaining05', {'a': 'a < b'}) + self.assertEqual(output, 'A < b') + self.assertEqual(len(warns), 1) + self.assertEqual( + str(warns[0].message), + "escape isn't the last filter in ['escape_filter', 'capfirst'] and " + "will be applied immediately in Django 2.0 so the output may change." + ) + + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'chaining06': '{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}'}) def test_chaining06(self): output = self.engine.render_to_string('chaining06', {'a': 'a < b'}) diff --git a/tests/template_tests/filter_tests/test_escape.py b/tests/template_tests/filter_tests/test_escape.py index 7dba5e1637..644ed7ac9e 100644 --- a/tests/template_tests/filter_tests/test_escape.py +++ b/tests/template_tests/filter_tests/test_escape.py @@ -1,6 +1,7 @@ from django.template.defaultfilters import escape -from django.test import SimpleTestCase +from django.test import SimpleTestCase, ignore_warnings from django.utils import six +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.functional import Promise, lazy from django.utils.safestring import mark_safe @@ -24,12 +25,14 @@ class EscapeTests(SimpleTestCase): self.assertEqual(output, "x&y x&y") # It is only applied once, regardless of the number of times it - # appears in a chain. + # appears in a chain (to be changed in Django 2.0). + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'escape03': '{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}'}) def test_escape03(self): output = self.engine.render_to_string('escape03', {"a": "x&y"}) self.assertEqual(output, "x&y") + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'escape04': '{{ a|escape|escape }}'}) def test_escape04(self): output = self.engine.render_to_string('escape04', {"a": "x&y"}) diff --git a/tests/template_tests/filter_tests/test_force_escape.py b/tests/template_tests/filter_tests/test_force_escape.py index 875ecb0ad9..f163f2cd75 100644 --- a/tests/template_tests/filter_tests/test_force_escape.py +++ b/tests/template_tests/filter_tests/test_force_escape.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals from django.template.defaultfilters import force_escape -from django.test import SimpleTestCase +from django.test import SimpleTestCase, ignore_warnings +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.safestring import SafeData from ..utils import setup @@ -35,7 +36,8 @@ class ForceEscapeTests(SimpleTestCase): self.assertEqual(output, "x&amp;y") # Because the result of force_escape is "safe", an additional - # escape filter has no effect. + # escape filter has no effect (to be changed in Django 2.0). + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'force-escape05': '{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}'}) def test_force_escape05(self): output = self.engine.render_to_string('force-escape05', {"a": "x&y"}) @@ -46,11 +48,13 @@ class ForceEscapeTests(SimpleTestCase): output = self.engine.render_to_string('force-escape06', {"a": "x&y"}) self.assertEqual(output, "x&y") + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'force-escape07': '{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}'}) def test_force_escape07(self): output = self.engine.render_to_string('force-escape07', {"a": "x&y"}) self.assertEqual(output, "x&y") + @ignore_warnings(category=RemovedInDjango20Warning) @setup({'force-escape08': '{{ a|escape|force_escape }}'}) def test_force_escape08(self): output = self.engine.render_to_string('force-escape08', {"a": "x&y"}) diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py index 7cc92a1370..6ea3972f78 100644 --- a/tests/utils_tests/test_safestring.py +++ b/tests/utils_tests/test_safestring.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals from django.template import Context, Template -from django.test import SimpleTestCase +from django.test import SimpleTestCase, ignore_warnings from django.utils import html, six, text +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_bytes from django.utils.functional import lazy, lazystr from django.utils.safestring import ( @@ -62,11 +63,13 @@ class SafeStringTest(SimpleTestCase): def test_mark_safe_lazy_result_implements_dunder_html(self): self.assertEqual(mark_safe(lazystr('a&b')).__html__(), 'a&b') + @ignore_warnings(category=RemovedInDjango20Warning) def test_mark_for_escaping(self): s = mark_for_escaping('a&b') self.assertRenderEqual('{{ s }}', 'a&b', s=s) self.assertRenderEqual('{{ s }}', 'a&b', s=mark_for_escaping(s)) + @ignore_warnings(category=RemovedInDjango20Warning) def test_mark_for_escaping_object_implementing_dunder_html(self): e = customescape('') s = mark_for_escaping(e) @@ -75,6 +78,7 @@ class SafeStringTest(SimpleTestCase): self.assertRenderEqual('{{ s }}', '<>', s=s) self.assertRenderEqual('{{ s|force_escape }}', '<a&b>', s=s) + @ignore_warnings(category=RemovedInDjango20Warning) def test_mark_for_escaping_lazy(self): s = lazystr('a&b') b = lazybytes(b'a&b') @@ -83,6 +87,7 @@ class SafeStringTest(SimpleTestCase): self.assertIsInstance(mark_for_escaping(b), EscapeData) self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&b', s=mark_for_escaping(s)) + @ignore_warnings(category=RemovedInDjango20Warning) def test_mark_for_escaping_object_implementing_dunder_str(self): class Obj(object): def __str__(self):