Fixed #24046 -- Deprecated the "escape" half of utils.safestring.

This commit is contained in:
Tim Graham 2016-05-10 12:46:47 -04:00
parent c3e1086949
commit 2f0e0eee45
13 changed files with 87 additions and 20 deletions

View File

@ -54,6 +54,7 @@ from __future__ import unicode_literals
import inspect import inspect
import logging import logging
import re import re
import warnings
from django.template.context import ( # NOQA: imported for backwards compatibility from django.template.context import ( # NOQA: imported for backwards compatibility
BaseContext, Context, ContextPopException, RequestContext, BaseContext, Context, ContextPopException, RequestContext,
@ -722,6 +723,7 @@ class FilterExpression(object):
obj = string_if_invalid obj = string_if_invalid
else: else:
obj = self.var obj = self.var
escape_isnt_last_filter = True
for func, args in self.filters: for func, args in self.filters:
arg_vals = [] arg_vals = []
for lookup, arg in args: for lookup, arg in args:
@ -738,9 +740,21 @@ class FilterExpression(object):
if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): if getattr(func, 'is_safe', False) and isinstance(obj, SafeData):
obj = mark_safe(new_obj) obj = mark_safe(new_obj)
elif isinstance(obj, EscapeData): 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: else:
obj = new_obj 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 return obj
def args_check(name, func, provided): def args_check(name, func, provided):

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import random as random_module import random as random_module
import re import re
import warnings
from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation
from functools import wraps from functools import wraps
from operator import itemgetter from operator import itemgetter
@ -10,6 +11,7 @@ from pprint import pformat
from django.utils import formats, six from django.utils import formats, six
from django.utils.dateformat import format, time_format 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.encoding import force_text, iri_to_uri
from django.utils.html import ( from django.utils.html import (
avoid_wrapping, conditional_escape, escape, escapejs, linebreaks, 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. 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) @register.filter(is_safe=True)

View File

@ -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 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. be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
""" """
import warnings
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import Promise, curry 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 Can be called multiple times on a single string (the resulting escaping is
only applied once). only applied once).
""" """
warnings.warn('mark_for_escaping() is deprecated.', RemovedInDjango20Warning)
if hasattr(s, '__html__') or isinstance(s, EscapeData): if hasattr(s, '__html__') or isinstance(s, EscapeData):
return s return s
if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes):

View File

@ -210,15 +210,6 @@ passed around inside the template code:
# Do something with the "safe" string. # 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: Template filter code falls into one of two situations:
1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``, 1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``,

View File

@ -168,6 +168,13 @@ details on these changes.
* ``FileField`` methods ``get_directory_name()`` and ``get_filename()`` will be * ``FileField`` methods ``get_directory_name()`` and ``get_filename()`` will be
removed. 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: .. _deprecation-removed-in-1.10:
1.10 1.10

View File

@ -1578,6 +1578,12 @@ For example, you can apply ``escape`` to fields when :ttag:`autoescape` is off::
{{ title|escape }} {{ title|escape }}
{% endautoescape %} {% 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 .. templatefilter:: escapejs
``escapejs`` ``escapejs``

View File

@ -839,6 +839,8 @@ appropriate entities.
.. function:: mark_for_escaping(s) .. function:: mark_for_escaping(s)
.. deprecated:: 1.10
Explicitly mark a string as requiring HTML escaping upon output. Has no Explicitly mark a string as requiring HTML escaping upon output. Has no
effect on ``SafeData`` subclasses. effect on ``SafeData`` subclasses.

View File

@ -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 as its single argument and return a filtered version of the queryset for the
model instance the manager is bound to. 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 Miscellaneous
------------- -------------

View File

@ -177,7 +177,7 @@ Bugfixes
setup (:ticket:`24000`). setup (:ticket:`24000`).
* Restored support for objects that aren't :class:`str` or :class:`bytes` in * 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__`` * Supported strings escaped by third-party libraries with the ``__html__``
convention in the template engine (:ticket:`23831`). convention in the template engine (:ticket:`23831`).

View File

@ -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 django.utils.safestring import mark_safe
from ..utils import setup from ..utils import setup
@ -38,9 +41,19 @@ class ChainingTests(SimpleTestCase):
# Using a filter that forces safeness does not lead to double-escaping # Using a filter that forces safeness does not lead to double-escaping
@setup({'chaining05': '{{ a|escape|capfirst }}'}) @setup({'chaining05': '{{ a|escape|capfirst }}'})
def test_chaining05(self): def test_chaining05(self):
output = self.engine.render_to_string('chaining05', {'a': 'a < b'}) with warnings.catch_warnings(record=True) as warns:
self.assertEqual(output, 'A &lt; b') warnings.simplefilter('always')
output = self.engine.render_to_string('chaining05', {'a': 'a < b'})
self.assertEqual(output, 'A &lt; 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 %}'}) @setup({'chaining06': '{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}'})
def test_chaining06(self): def test_chaining06(self):
output = self.engine.render_to_string('chaining06', {'a': 'a < b'}) output = self.engine.render_to_string('chaining06', {'a': 'a < b'})

View File

@ -1,6 +1,7 @@
from django.template.defaultfilters import escape 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 import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import Promise, lazy from django.utils.functional import Promise, lazy
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -24,12 +25,14 @@ class EscapeTests(SimpleTestCase):
self.assertEqual(output, "x&amp;y x&y") self.assertEqual(output, "x&amp;y x&y")
# It is only applied once, regardless of the number of times it # 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 %}'}) @setup({'escape03': '{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}'})
def test_escape03(self): def test_escape03(self):
output = self.engine.render_to_string('escape03', {"a": "x&y"}) output = self.engine.render_to_string('escape03', {"a": "x&y"})
self.assertEqual(output, "x&amp;y") self.assertEqual(output, "x&amp;y")
@ignore_warnings(category=RemovedInDjango20Warning)
@setup({'escape04': '{{ a|escape|escape }}'}) @setup({'escape04': '{{ a|escape|escape }}'})
def test_escape04(self): def test_escape04(self):
output = self.engine.render_to_string('escape04', {"a": "x&y"}) output = self.engine.render_to_string('escape04', {"a": "x&y"})

View File

@ -2,7 +2,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.template.defaultfilters import force_escape 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 django.utils.safestring import SafeData
from ..utils import setup from ..utils import setup
@ -35,7 +36,8 @@ class ForceEscapeTests(SimpleTestCase):
self.assertEqual(output, "x&amp;amp;y") self.assertEqual(output, "x&amp;amp;y")
# Because the result of force_escape is "safe", an additional # 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 %}'}) @setup({'force-escape05': '{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}'})
def test_force_escape05(self): def test_force_escape05(self):
output = self.engine.render_to_string('force-escape05', {"a": "x&y"}) 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"}) output = self.engine.render_to_string('force-escape06', {"a": "x&y"})
self.assertEqual(output, "x&amp;y") self.assertEqual(output, "x&amp;y")
@ignore_warnings(category=RemovedInDjango20Warning)
@setup({'force-escape07': '{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}'}) @setup({'force-escape07': '{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}'})
def test_force_escape07(self): def test_force_escape07(self):
output = self.engine.render_to_string('force-escape07', {"a": "x&y"}) output = self.engine.render_to_string('force-escape07', {"a": "x&y"})
self.assertEqual(output, "x&amp;y") self.assertEqual(output, "x&amp;y")
@ignore_warnings(category=RemovedInDjango20Warning)
@setup({'force-escape08': '{{ a|escape|force_escape }}'}) @setup({'force-escape08': '{{ a|escape|force_escape }}'})
def test_force_escape08(self): def test_force_escape08(self):
output = self.engine.render_to_string('force-escape08', {"a": "x&y"}) output = self.engine.render_to_string('force-escape08', {"a": "x&y"})

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals 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, ignore_warnings
from django.utils import html, six, text from django.utils import html, six, text
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.functional import lazy, lazystr from django.utils.functional import lazy, lazystr
from django.utils.safestring import ( from django.utils.safestring import (
@ -62,11 +63,13 @@ class SafeStringTest(SimpleTestCase):
def test_mark_safe_lazy_result_implements_dunder_html(self): def test_mark_safe_lazy_result_implements_dunder_html(self):
self.assertEqual(mark_safe(lazystr('a&b')).__html__(), 'a&b') self.assertEqual(mark_safe(lazystr('a&b')).__html__(), 'a&b')
@ignore_warnings(category=RemovedInDjango20Warning)
def test_mark_for_escaping(self): def test_mark_for_escaping(self):
s = mark_for_escaping('a&b') s = mark_for_escaping('a&b')
self.assertRenderEqual('{{ s }}', 'a&amp;b', s=s) self.assertRenderEqual('{{ s }}', 'a&amp;b', s=s)
self.assertRenderEqual('{{ s }}', 'a&amp;b', s=mark_for_escaping(s)) self.assertRenderEqual('{{ s }}', 'a&amp;b', s=mark_for_escaping(s))
@ignore_warnings(category=RemovedInDjango20Warning)
def test_mark_for_escaping_object_implementing_dunder_html(self): def test_mark_for_escaping_object_implementing_dunder_html(self):
e = customescape('<a&b>') e = customescape('<a&b>')
s = mark_for_escaping(e) s = mark_for_escaping(e)
@ -75,6 +78,7 @@ class SafeStringTest(SimpleTestCase):
self.assertRenderEqual('{{ s }}', '<<a&b>>', s=s) self.assertRenderEqual('{{ s }}', '<<a&b>>', s=s)
self.assertRenderEqual('{{ s|force_escape }}', '&lt;a&amp;b&gt;', s=s) self.assertRenderEqual('{{ s|force_escape }}', '&lt;a&amp;b&gt;', s=s)
@ignore_warnings(category=RemovedInDjango20Warning)
def test_mark_for_escaping_lazy(self): def test_mark_for_escaping_lazy(self):
s = lazystr('a&b') s = lazystr('a&b')
b = lazybytes(b'a&b') b = lazybytes(b'a&b')
@ -83,6 +87,7 @@ class SafeStringTest(SimpleTestCase):
self.assertIsInstance(mark_for_escaping(b), EscapeData) self.assertIsInstance(mark_for_escaping(b), EscapeData)
self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&amp;b', s=mark_for_escaping(s)) self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&amp;b', s=mark_for_escaping(s))
@ignore_warnings(category=RemovedInDjango20Warning)
def test_mark_for_escaping_object_implementing_dunder_str(self): def test_mark_for_escaping_object_implementing_dunder_str(self):
class Obj(object): class Obj(object):
def __str__(self): def __str__(self):