2
0
Эх сурвалжийг харах

Fixed #17135 -- Made it possible to use decorators (like stringfilter) on template filter functions in combination with auto-escaping. Refs #16726.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@17056 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Aymeric Augustin 13 жил өмнө
parent
commit
d17bc72880

+ 4 - 8
django/contrib/humanize/templatetags/humanize.py

@@ -11,7 +11,7 @@ from django.utils.tzinfo import LocalTimezone
 
 register = template.Library()
 
-@register.filter
+@register.filter(is_safe=True)
 def ordinal(value):
     """
     Converts an integer to its ordinal as a string. 1 is '1st', 2 is '2nd',
@@ -25,9 +25,8 @@ def ordinal(value):
     if value % 100 in (11, 12, 13): # special case
         return u"%d%s" % (value, suffixes[0])
     return u"%d%s" % (value, suffixes[value % 10])
-ordinal.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def intcomma(value, use_l10n=True):
     """
     Converts an integer to a string containing commas every three digits.
@@ -47,7 +46,6 @@ def intcomma(value, use_l10n=True):
         return new
     else:
         return intcomma(new, use_l10n)
-intcomma.is_safe = True
 
 # A tuple of standard large number to their converters
 intword_converters = (
@@ -97,7 +95,7 @@ intword_converters = (
     )),
 )
 
-@register.filter
+@register.filter(is_safe=False)
 def intword(value):
     """
     Converts a large integer to a friendly text representation. Works best
@@ -129,9 +127,8 @@ def intword(value):
             new_value = value / float(large_number)
             return _check_for_i18n(new_value, *converters(new_value))
     return value
-intword.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True)
 def apnumber(value):
     """
     For numbers 1-9, returns the number spelled out. Otherwise, returns the
@@ -144,7 +141,6 @@ def apnumber(value):
     if not 0 < value < 10:
         return value
     return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1]
-apnumber.is_safe = True
 
 @register.filter
 def naturalday(value, arg=None):

+ 3 - 7
django/contrib/markup/templatetags/markup.py

@@ -18,7 +18,7 @@ from django.utils.safestring import mark_safe
 
 register = template.Library()
 
-@register.filter
+@register.filter(is_safe=True)
 def textile(value):
     try:
         import textile
@@ -28,9 +28,8 @@ def textile(value):
         return force_unicode(value)
     else:
         return mark_safe(force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8')))
-textile.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def markdown(value, arg=''):
     """
     Runs Markdown over a given value, optionally using various
@@ -73,9 +72,8 @@ def markdown(value, arg=''):
                 return mark_safe(markdown.markdown(force_unicode(value), extensions, safe_mode=safe_mode))
         else:
             return mark_safe(force_unicode(markdown.markdown(smart_str(value))))
-markdown.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def restructuredtext(value):
     try:
         from docutils.core import publish_parts
@@ -87,5 +85,3 @@ def restructuredtext(value):
         docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {})
         parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings)
         return mark_safe(force_unicode(parts["fragment"]))
-restructuredtext.is_safe = True
-

+ 21 - 8
django/template/base.py

@@ -1057,30 +1057,43 @@ class Library(object):
         self.tags[getattr(func, "_decorated_function", func).__name__] = func
         return func
 
-    def filter(self, name=None, filter_func=None):
+    def filter(self, name=None, filter_func=None, **flags):
         if name is None and filter_func is None:
             # @register.filter()
-            return self.filter_function
-        elif filter_func is None:
+            def dec(func):
+                return self.filter_function(func, **flags)
+            return dec
+
+        elif name is not None and filter_func is None:
             if callable(name):
                 # @register.filter
-                return self.filter_function(name)
+                return self.filter_function(name, **flags)
             else:
                 # @register.filter('somename') or @register.filter(name='somename')
                 def dec(func):
-                    return self.filter(name, func)
+                    return self.filter(name, func, **flags)
                 return dec
+
         elif name is not None and filter_func is not None:
             # register.filter('somename', somefunc)
             self.filters[name] = filter_func
+            for attr in ('is_safe', 'needs_autoescape'):
+                if attr in flags:
+                    value = flags[attr]
+                    # set the flag on the filter for FilterExpression.resolve
+                    setattr(filter_func, attr, value)
+                    # set the flag on the innermost decorated function
+                    # for decorators that need it e.g. stringfilter
+                    if hasattr(filter_func, "_decorated_function"):
+                        setattr(filter_func._decorated_function, attr, value)
             return filter_func
         else:
             raise InvalidTemplateLibrary("Unsupported arguments to "
                 "Library.filter: (%r, %r)", (name, filter_func))
 
-    def filter_function(self, func):
-        self.filters[getattr(func, "_decorated_function", func).__name__] = func
-        return func
+    def filter_function(self, func, **flags):
+        name = getattr(func, "_decorated_function", func).__name__
+        return self.filter(name, func, **flags)
 
     def simple_tag(self, func=None, takes_context=None, name=None):
         def dec(func):

+ 67 - 119
django/template/defaultfilters.py

@@ -37,23 +37,33 @@ def stringfilter(func):
         if args:
             args = list(args)
             args[0] = force_unicode(args[0])
-            if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False):
+            if (isinstance(args[0], SafeData) and
+                getattr(_dec._decorated_function, 'is_safe', False)):
                 return mark_safe(func(*args, **kwargs))
         return func(*args, **kwargs)
 
     # Include a reference to the real function (used to check original
-    # arguments by the template parser).
+    # arguments by the template parser, and to bear the 'is_safe' attribute
+    # when multiple decorators are applied).
     _dec._decorated_function = getattr(func, '_decorated_function', func)
+
     for attr in ('is_safe', 'needs_autoescape'):
         if hasattr(func, attr):
+            import warnings
+            warnings.warn("Setting the %s attribute of a template filter "
+                          "function is deprecated; use @register.filter(%s=%s) "
+                          "instead" % (attr, attr, getattr(func, attr)),
+                          PendingDeprecationWarning)
             setattr(_dec, attr, getattr(func, attr))
+
     return wraps(func)(_dec)
 
+
 ###################
 # STRINGS         #
 ###################
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def addslashes(value):
     """
@@ -62,14 +72,12 @@ def addslashes(value):
     filter instead.
     """
     return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
-addslashes.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def capfirst(value):
     """Capitalizes the first character of the value."""
     return value and value[0].upper() + value[1:]
-capfirst.is_safe = True
 
 @register.filter("escapejs")
 @stringfilter
@@ -77,12 +85,11 @@ def escapejs_filter(value):
     """Hex encodes characters for use in JavaScript strings."""
     return escapejs(value)
 
-@register.filter("fix_ampersands")
+@register.filter("fix_ampersands", is_safe=True)
 @stringfilter
 def fix_ampersands_filter(value):
     """Replaces ampersands with ``&amp;`` entities."""
     return fix_ampersands(value)
-fix_ampersands_filter.is_safe = True
 
 # Values for testing floatformat input against infinity and NaN representations,
 # which differ across platforms and Python versions.  Some (i.e. old Windows
@@ -96,7 +103,7 @@ neg_inf = -1e200 * 1e200
 nan = (1e200 * 1e200) // (1e200 * 1e200)
 special_floats = [str(pos_inf), str(neg_inf), str(nan)]
 
-@register.filter
+@register.filter(is_safe=True)
 def floatformat(text, arg=-1):
     """
     Displays a float to a specified number of decimal places.
@@ -172,16 +179,14 @@ def floatformat(text, arg=-1):
         return mark_safe(formats.number_format(number, abs(p)))
     except InvalidOperation:
         return input_val
-floatformat.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def iriencode(value):
     """Escapes an IRI value for use in a URL."""
     return force_unicode(iri_to_uri(value))
-iriencode.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 @stringfilter
 def linenumbers(value, autoescape=None):
     """Displays text with line numbers."""
@@ -196,17 +201,14 @@ def linenumbers(value, autoescape=None):
         for i, line in enumerate(lines):
             lines[i] = (u"%0" + width  + u"d. %s") % (i + 1, escape(line))
     return mark_safe(u'\n'.join(lines))
-linenumbers.is_safe = True
-linenumbers.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def lower(value):
     """Converts a string into all lowercase."""
     return value.lower()
-lower.is_safe = True
 
-@register.filter
+@register.filter(is_safe=False)
 @stringfilter
 def make_list(value):
     """
@@ -216,9 +218,8 @@ def make_list(value):
     For a string, it's a list of characters.
     """
     return list(value)
-make_list.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def slugify(value):
     """
@@ -228,9 +229,8 @@ def slugify(value):
     value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
     value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
     return mark_safe(re.sub('[-\s]+', '-', value))
-slugify.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def stringformat(value, arg):
     """
     Formats the variable according to the arg, a string formatting specifier.
@@ -245,17 +245,15 @@ def stringformat(value, arg):
         return (u"%" + unicode(arg)) % value
     except (ValueError, TypeError):
         return u""
-stringformat.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def title(value):
     """Converts a string into titlecase."""
     t = re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
     return re.sub("\d([A-Z])", lambda m: m.group(0).lower(), t)
-title.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def truncatechars(value, arg):
     """
@@ -268,9 +266,8 @@ def truncatechars(value, arg):
     except ValueError: # Invalid literal for int().
         return value # Fail silently.
     return Truncator(value).chars(length)
-truncatechars.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def truncatewords(value, arg):
     """
@@ -285,9 +282,8 @@ def truncatewords(value, arg):
     except ValueError: # Invalid literal for int().
         return value # Fail silently.
     return Truncator(value).words(length, truncate=' ...')
-truncatewords.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def truncatewords_html(value, arg):
     """
@@ -302,16 +298,14 @@ def truncatewords_html(value, arg):
     except ValueError: # invalid literal for int()
         return value # Fail silently.
     return Truncator(value).words(length, html=True, truncate=' ...')
-truncatewords_html.is_safe = True
 
-@register.filter
+@register.filter(is_safe=False)
 @stringfilter
 def upper(value):
     """Converts a string into all uppercase."""
     return value.upper()
-upper.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 @stringfilter
 def urlencode(value, safe=None):
     """
@@ -326,17 +320,14 @@ def urlencode(value, safe=None):
     if safe is not None:
         kwargs['safe'] = safe
     return urlquote(value, **kwargs)
-urlencode.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 @stringfilter
 def urlize(value, autoescape=None):
     """Converts URLs in plain text into clickable links."""
     return mark_safe(urlize_impl(value, nofollow=True, autoescape=autoescape))
-urlize.is_safe = True
-urlize.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 @stringfilter
 def urlizetrunc(value, limit, autoescape=None):
     """
@@ -347,17 +338,14 @@ def urlizetrunc(value, limit, autoescape=None):
     """
     return mark_safe(urlize_impl(value, trim_url_limit=int(limit), nofollow=True,
                             autoescape=autoescape))
-urlizetrunc.is_safe = True
-urlizetrunc.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=False)
 @stringfilter
 def wordcount(value):
     """Returns the number of words."""
     return len(value.split())
-wordcount.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def wordwrap(value, arg):
     """
@@ -366,9 +354,8 @@ def wordwrap(value, arg):
     Argument: number of characters to wrap the text at.
     """
     return wrap(value, int(arg))
-wordwrap.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def ljust(value, arg):
     """
@@ -377,9 +364,8 @@ def ljust(value, arg):
     Argument: field size.
     """
     return value.ljust(int(arg))
-ljust.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def rjust(value, arg):
     """
@@ -388,14 +374,12 @@ def rjust(value, arg):
     Argument: field size.
     """
     return value.rjust(int(arg))
-rjust.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def center(value, arg):
     """Centers the value in a field of a given width."""
     return value.center(int(arg))
-center.is_safe = True
 
 @register.filter
 @stringfilter
@@ -413,16 +397,15 @@ def cut(value, arg):
 # HTML STRINGS    #
 ###################
 
-@register.filter("escape")
+@register.filter("escape", is_safe=True)
 @stringfilter
 def escape_filter(value):
     """
     Marks the value as a string that should not be auto-escaped.
     """
     return mark_for_escaping(value)
-escape_filter.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def force_escape(value):
     """
@@ -431,9 +414,8 @@ def force_escape(value):
     possible escaping).
     """
     return mark_safe(escape(value))
-force_escape.is_safe = True
 
-@register.filter("linebreaks")
+@register.filter("linebreaks", is_safe=True, needs_autoescape=True)
 @stringfilter
 def linebreaks_filter(value, autoescape=None):
     """
@@ -443,10 +425,8 @@ def linebreaks_filter(value, autoescape=None):
     """
     autoescape = autoescape and not isinstance(value, SafeData)
     return mark_safe(linebreaks(value, autoescape))
-linebreaks_filter.is_safe = True
-linebreaks_filter.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 @stringfilter
 def linebreaksbr(value, autoescape=None):
     """
@@ -458,19 +438,16 @@ def linebreaksbr(value, autoescape=None):
     if autoescape:
         value = escape(value)
     return mark_safe(value.replace('\n', '<br />'))
-linebreaksbr.is_safe = True
-linebreaksbr.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def safe(value):
     """
     Marks the value as a string that should not be auto-escaped.
     """
     return mark_safe(value)
-safe.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def safeseq(value):
     """
     A "safe" filter for sequences. Marks each element in the sequence,
@@ -478,9 +455,8 @@ def safeseq(value):
     with the results.
     """
     return [mark_safe(force_unicode(obj)) for obj in value]
-safeseq.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def removetags(value, tags):
     """Removes a space separated list of [X]HTML tags from the output."""
@@ -491,47 +467,42 @@ def removetags(value, tags):
     value = starttag_re.sub(u'', value)
     value = endtag_re.sub(u'', value)
     return value
-removetags.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 @stringfilter
 def striptags(value):
     """Strips all [X]HTML tags."""
     return strip_tags(value)
-striptags.is_safe = True
 
 ###################
 # LISTS           #
 ###################
 
-@register.filter
+@register.filter(is_safe=False)
 def dictsort(value, arg):
     """
     Takes a list of dicts, returns that list sorted by the property given in
     the argument.
     """
     return sorted(value, key=Variable(arg).resolve)
-dictsort.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def dictsortreversed(value, arg):
     """
     Takes a list of dicts, returns that list sorted in reverse order by the
     property given in the argument.
     """
     return sorted(value, key=Variable(arg).resolve, reverse=True)
-dictsortreversed.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def first(value):
     """Returns the first item in a list."""
     try:
         return value[0]
     except IndexError:
         return u''
-first.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 def join(value, arg, autoescape=None):
     """
     Joins a list with a string, like Python's ``str.join(list)``.
@@ -544,43 +515,37 @@ def join(value, arg, autoescape=None):
     except AttributeError: # fail silently but nicely
         return value
     return mark_safe(data)
-join.is_safe = True
-join.needs_autoescape = True
 
-@register.filter
+@register.filter(is_safe=True)
 def last(value):
     "Returns the last item in a list"
     try:
         return value[-1]
     except IndexError:
         return u''
-last.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def length(value):
     """Returns the length of the value - useful for lists."""
     try:
         return len(value)
     except (ValueError, TypeError):
         return ''
-length.is_safe = True
 
-@register.filter
+@register.filter(is_safe=False)
 def length_is(value, arg):
     """Returns a boolean of whether the value's length is the argument."""
     try:
         return len(value) == int(arg)
     except (ValueError, TypeError):
         return ''
-length_is.is_safe = False
 
-@register.filter
+@register.filter(is_safe=True)
 def random(value):
     """Returns a random item from the list."""
     return random_module.choice(value)
-random.is_safe = True
 
-@register.filter("slice")
+@register.filter("slice", is_safe=True)
 def slice_filter(value, arg):
     """
     Returns a slice of the list.
@@ -600,9 +565,8 @@ def slice_filter(value, arg):
 
     except (ValueError, TypeError):
         return value # Fail silently.
-slice_filter.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True, needs_autoescape=True)
 def unordered_list(value, autoescape=None):
     """
     Recursively takes a self-nested list and returns an HTML unordered list --
@@ -688,14 +652,12 @@ def unordered_list(value, autoescape=None):
         return '\n'.join(output)
     value, converted = convert_old_style_list(value)
     return mark_safe(_helper(value))
-unordered_list.is_safe = True
-unordered_list.needs_autoescape = True
 
 ###################
 # INTEGERS        #
 ###################
 
-@register.filter
+@register.filter(is_safe=False)
 def add(value, arg):
     """Adds the arg to the value."""
     try:
@@ -705,9 +667,8 @@ def add(value, arg):
             return value + arg
         except Exception:
             return ''
-add.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def get_digit(value, arg):
     """
     Given a whole number, returns the requested digit of it, where 1 is the
@@ -726,13 +687,12 @@ def get_digit(value, arg):
         return int(str(value)[-arg])
     except IndexError:
         return 0
-get_digit.is_safe = False
 
 ###################
 # DATES           #
 ###################
 
-@register.filter
+@register.filter(is_safe=False)
 def date(value, arg=None):
     """Formats a date according to the given format."""
     if not value:
@@ -746,9 +706,8 @@ def date(value, arg=None):
             return format(value, arg)
         except AttributeError:
             return ''
-date.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def time(value, arg=None):
     """Formats a time according to the given format."""
     if value in (None, u''):
@@ -762,9 +721,8 @@ def time(value, arg=None):
             return time_format(value, arg)
         except AttributeError:
             return ''
-time.is_safe = False
 
-@register.filter("timesince")
+@register.filter("timesince", is_safe=False)
 def timesince_filter(value, arg=None):
     """Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
     if not value:
@@ -775,9 +733,8 @@ def timesince_filter(value, arg=None):
         return timesince(value)
     except (ValueError, TypeError):
         return u''
-timesince_filter.is_safe = False
 
-@register.filter("timeuntil")
+@register.filter("timeuntil", is_safe=False)
 def timeuntil_filter(value, arg=None):
     """Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
     if not value:
@@ -786,33 +743,29 @@ def timeuntil_filter(value, arg=None):
         return timeuntil(value, arg)
     except (ValueError, TypeError):
         return u''
-timeuntil_filter.is_safe = False
 
 ###################
 # LOGIC           #
 ###################
 
-@register.filter
+@register.filter(is_safe=False)
 def default(value, arg):
     """If value is unavailable, use given default."""
     return value or arg
-default.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def default_if_none(value, arg):
     """If value is None, use given default."""
     if value is None:
         return arg
     return value
-default_if_none.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def divisibleby(value, arg):
     """Returns True if the value is devisible by the argument."""
     return int(value) % int(arg) == 0
-divisibleby.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def yesno(value, arg=None):
     """
     Given a string mapping values for true, false and (optionally) None,
@@ -843,13 +796,12 @@ def yesno(value, arg=None):
     if value:
         return yes
     return no
-yesno.is_safe = False
 
 ###################
 # MISC            #
 ###################
 
-@register.filter
+@register.filter(is_safe=True)
 def filesizeformat(bytes):
     """
     Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
@@ -873,9 +825,8 @@ def filesizeformat(bytes):
     if bytes < 1024 * 1024 * 1024 * 1024 * 1024:
         return ugettext("%s TB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024))
     return ugettext("%s PB") % filesize_number_format(bytes / (1024 * 1024 * 1024 * 1024 * 1024))
-filesizeformat.is_safe = True
 
-@register.filter
+@register.filter(is_safe=False)
 def pluralize(value, arg=u's'):
     """
     Returns a plural suffix if the value is not 1. By default, 's' is used as
@@ -918,19 +869,16 @@ def pluralize(value, arg=u's'):
         except TypeError: # len() of unsized object.
             pass
     return singular_suffix
-pluralize.is_safe = False
 
-@register.filter("phone2numeric")
+@register.filter("phone2numeric", is_safe=True)
 def phone2numeric_filter(value):
     """Takes a phone number and converts it in to its numerical equivalent."""
     return phone2numeric(value)
-phone2numeric_filter.is_safe = True
 
-@register.filter
+@register.filter(is_safe=True)
 def pprint(value):
     """A wrapper around pprint.pprint -- for debugging, really."""
     try:
         return pformat(value)
     except Exception, e:
         return u"Error in formatting: %s" % force_unicode(e, errors="replace")
-pprint.is_safe = True

+ 2 - 4
django/templatetags/l10n.py

@@ -5,23 +5,21 @@ from django.utils.encoding import force_unicode
 
 register = Library()
 
-@register.filter
+@register.filter(is_safe=False)
 def localize(value):
     """
     Forces a value to be rendered as a localized value,
     regardless of the value of ``settings.USE_L10N``.
     """
     return force_unicode(formats.localize(value, use_l10n=True))
-localize.is_safe = False
 
-@register.filter
+@register.filter(is_safe=False)
 def unlocalize(value):
     """
     Forces a value to be rendered as a non-localized value,
     regardless of the value of ``settings.USE_L10N``.
     """
     return force_unicode(value)
-unlocalize.is_safe = False
 
 class LocalizeNode(Node):
     def __init__(self, nodelist, use_l10n):

+ 52 - 29
docs/howto/custom-template-tags.txt

@@ -143,6 +143,10 @@ You can use ``register.filter()`` as a decorator instead:
 If you leave off the ``name`` argument, as in the second example above, Django
 will use the function's name as the filter name.
 
+Finally, ``register.filter()`` also accepts two keyword arguments, ``is_safe``
+and ``needs_autoescape``, described in :ref:`filters and auto-escaping
+<filters-auto-escaping>` below.
+
 Template filters that expect strings
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -166,6 +170,8 @@ This way, you'll be able to pass, say, an integer to this filter, and it
 won't cause an ``AttributeError`` (because integers don't have ``lower()``
 methods).
 
+.. _filters-auto-escaping:
+
 Filters and auto-escaping
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -206,17 +212,16 @@ Template filter code falls into one of two situations:
 1. Your filter does not introduce any HTML-unsafe characters (``<``, ``>``,
    ``'``, ``"`` or ``&``) into the result that were not already present. In
    this case, you can let Django take care of all the auto-escaping
-   handling for you. All you need to do is put the ``is_safe`` attribute on
-   your filter function and set it to ``True``, like so:
+   handling for you. All you need to do is set the ``is_safe`` flag to ``True``
+   when you register your filter function, like so:
 
    .. code-block:: python
 
-       @register.filter
+       @register.filter(is_safe=True)
        def myfilter(value):
            return value
-       myfilter.is_safe = True
 
-   This attribute tells Django that if a "safe" string is passed into your
+   This flag tells Django that if a "safe" string is passed into your
    filter, the result will still be "safe" and if a non-safe string is
    passed in, Django will automatically escape it, if necessary.
 
@@ -236,17 +241,16 @@ Template filter code falls into one of two situations:
 
    .. code-block:: python
 
-       @register.filter
+       @register.filter(is_safe=True)
        def add_xx(value):
            return '%sxx' % value
-       add_xx.is_safe = True
 
    When this filter is used in a template where auto-escaping is enabled,
    Django will escape the output whenever the input is not already marked
    as "safe".
 
-   By default, ``is_safe`` defaults to ``False``, and you can omit it from
-   any filters where it isn't required.
+   By default, ``is_safe`` is ``False``, and you can omit it from any filters
+   where it isn't required.
 
    Be careful when deciding if your filter really does leave safe strings
    as safe. If you're *removing* characters, you might inadvertently leave
@@ -279,12 +283,12 @@ Template filter code falls into one of two situations:
    can operate in templates where auto-escaping is either on or off in
    order to make things easier for your template authors.
 
-   In order for your filter to know the current auto-escaping state, set
-   the ``needs_autoescape`` attribute to ``True`` on your function. (If you
-   don't specify this attribute, it defaults to ``False``). This attribute
-   tells Django that your filter function wants to be passed an extra
-   keyword argument, called ``autoescape``, that is ``True`` if
-   auto-escaping is in effect and ``False`` otherwise.
+   In order for your filter to know the current auto-escaping state, set the
+   ``needs_autoescape`` flag to ``True`` when you register your filter function.
+   (If you don't specify this flag, it defaults to ``False``). This flag tells
+   Django that your filter function wants to be passed an extra keyword
+   argument, called ``autoescape``, that is ``True`` if auto-escaping is in
+   effect and ``False`` otherwise.
 
    For example, let's write a filter that emphasizes the first character of
    a string:
@@ -294,6 +298,7 @@ Template filter code falls into one of two situations:
        from django.utils.html import conditional_escape
        from django.utils.safestring import mark_safe
 
+       @register.filter(needs_autoescape=True)
        def initial_letter_filter(text, autoescape=None):
            first, other = text[0], text[1:]
            if autoescape:
@@ -302,27 +307,45 @@ Template filter code falls into one of two situations:
                esc = lambda x: x
            result = '<strong>%s</strong>%s' % (esc(first), esc(other))
            return mark_safe(result)
-       initial_letter_filter.needs_autoescape = True
-
-   The ``needs_autoescape`` attribute on the filter function and the
-   ``autoescape`` keyword argument mean that our function will know whether
-   automatic escaping is in effect when the filter is called. We use
-   ``autoescape`` to decide whether the input data needs to be passed
-   through ``django.utils.html.conditional_escape`` or not. (In the latter
-   case, we just use the identity function as the "escape" function.) The
-   ``conditional_escape()`` function is like ``escape()`` except it only
-   escapes input that is **not** a ``SafeData`` instance. If a ``SafeData``
-   instance is passed to ``conditional_escape()``, the data is returned
-   unchanged.
+
+   The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean
+   that our function will know whether automatic escaping is in effect when the
+   filter is called. We use ``autoescape`` to decide whether the input data
+   needs to be passed through ``django.utils.html.conditional_escape`` or not.
+   (In the latter case, we just use the identity function as the "escape"
+   function.) The ``conditional_escape()`` function is like ``escape()`` except
+   it only escapes input that is **not** a ``SafeData`` instance. If a
+   ``SafeData`` instance is passed to ``conditional_escape()``, the data is
+   returned unchanged.
 
    Finally, in the above example, we remember to mark the result as safe
    so that our HTML is inserted directly into the template without further
    escaping.
 
-   There's no need to worry about the ``is_safe`` attribute in this case
+   There's no need to worry about the ``is_safe`` flag in this case
    (although including it wouldn't hurt anything). Whenever you manually
    handle the auto-escaping issues and return a safe string, the
-   ``is_safe`` attribute won't change anything either way.
+   ``is_safe`` flag won't change anything either way.
+
+.. versionchanged:: 1.4
+
+``is_safe`` and ``needs_autoescape`` used to be attributes of the filter
+function; this syntax is deprecated.
+
+.. code-block:: python
+
+    @register.filter
+    def myfilter(value):
+        return value
+    myfilter.is_safe = True
+
+.. code-block:: python
+
+    @register.filter
+    def initial_letter_filter(text, autoescape=None):
+        # ...
+        return mark_safe(result)
+    initial_letter_filter.needs_autoescape = True
 
 Writing custom template tags
 ----------------------------

+ 3 - 0
docs/internals/deprecation.txt

@@ -251,6 +251,9 @@ these changes.
   :mod:`django.core.management`. This also means that the old (pre-1.4)
   style of :file:`manage.py` file will no longer work.
 
+* Setting the ``is_safe`` and ``needs_autoescape`` flags as attributes of
+  template filter functions will no longer be supported.
+
 2.0
 ---
 

+ 3 - 4
tests/regressiontests/templates/templatetags/custom.py

@@ -6,11 +6,10 @@ from django.template.loader import get_template
 
 register = template.Library()
 
+@register.filter
+@stringfilter
 def trim(value, num):
     return value[:num]
-trim = stringfilter(trim)
-
-register.filter(trim)
 
 @register.simple_tag
 def no_params():
@@ -303,4 +302,4 @@ assignment_unlimited_args_kwargs.anything = "Expected assignment_unlimited_args_
 def assignment_tag_without_context_parameter(arg):
     """Expected assignment_tag_without_context_parameter __doc__"""
     return "Expected result"
-assignment_tag_without_context_parameter.anything = "Expected assignment_tag_without_context_parameter __dict__"
+assignment_tag_without_context_parameter.anything = "Expected assignment_tag_without_context_parameter __dict__"