Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #343: filters that take strings now handle non-strings correctl…

…y. Thanks to Boffbowsh for the original patch, and to SmileyChris for the updated patch and tests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4558 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 36512d5d734feee53b7787dbdf2d671f158ebaae 1 parent 1266766
Jacob Kaplan-Moss authored February 23, 2007
2  django/template/__init__.py
@@ -580,6 +580,8 @@ def resolve(self, context, ignore_failures=False):
580 580
     def args_check(name, func, provided):
581 581
         provided = list(provided)
582 582
         plen = len(provided)
  583
+        # Check to see if a decorator is providing the real function.
  584
+        func = getattr(func, '_decorated_function', func)
583 585
         args, varargs, varkw, defaults = getargspec(func)
584 586
         # First argument is filter input.
585 587
         args.pop(0)
82  django/template/defaultfilters.py
@@ -8,6 +8,42 @@
8 8
 
9 9
 register = Library()
10 10
 
  11
+#######################
  12
+# STRING DECORATOR    #
  13
+#######################
  14
+
  15
+def smart_string(obj):
  16
+    # FUTURE: Unicode strings should probably be normalized to a specific
  17
+    # encoding and non-unicode strings should be converted to unicode too.
  18
+#    if isinstance(obj, unicode):
  19
+#        obj = obj.encode(settings.DEFAULT_CHARSET)
  20
+#    else:
  21
+#        obj = unicode(obj, settings.DEFAULT_CHARSET)
  22
+    # FUTURE: Replace dumb string logic below with cool unicode logic above.
  23
+    if not isinstance(obj, basestring):
  24
+        obj = str(obj)
  25
+    return obj
  26
+
  27
+def stringfilter(func):
  28
+    """
  29
+    Decorator for filters which should only receive strings. The object passed
  30
+    as the first positional argument will be converted to a string.
  31
+    """
  32
+    def _dec(*args, **kwargs):
  33
+        if args:
  34
+            args = list(args)
  35
+            args[0] = smart_string(args[0])
  36
+        return func(*args, **kwargs)
  37
+        
  38
+    # Make sure the internal name is the original function name because this
  39
+    # is the internal name of the filter if passed directly to Library().filter
  40
+    _dec.__name__ = func.__name__
  41
+    
  42
+    # Include a reference to the real function (used to check original
  43
+    # arguments by the template parser).
  44
+    _dec._decorated_function = getattr(func, '_decorated_function', func)
  45
+    return _dec
  46
+
11 47
 ###################
12 48
 # STRINGS         #
13 49
 ###################
@@ -16,16 +52,18 @@
16 52
 def addslashes(value):
17 53
     "Adds slashes - useful for passing strings to JavaScript, for example."
18 54
     return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
  55
+addslashes = stringfilter(addslashes)
19 56
 
20 57
 def capfirst(value):
21 58
     "Capitalizes the first character of the value"
22  
-    value = str(value)
23 59
     return value and value[0].upper() + value[1:]
24  
-
  60
+capfirst = stringfilter(capfirst)
  61
+ 
25 62
 def fix_ampersands(value):
26 63
     "Replaces ampersands with ``&`` entities"
27 64
     from django.utils.html import fix_ampersands
28 65
     return fix_ampersands(value)
  66
+fix_ampersands = stringfilter(fix_ampersands)
29 67
 
30 68
 def floatformat(text, arg=-1):
31 69
     """
@@ -52,7 +90,7 @@ def floatformat(text, arg=-1):
52 90
     try:
53 91
         d = int(arg)
54 92
     except ValueError:
55  
-        return str(f)
  93
+        return smart_string(f)
56 94
     m = f - int(f)
57 95
     if not m and d < 0:
58 96
         return '%d' % int(f)
@@ -69,22 +107,26 @@ def linenumbers(value):
69 107
     for i, line in enumerate(lines):
70 108
         lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line))
71 109
     return '\n'.join(lines)
  110
+linenumbers = stringfilter(linenumbers)
72 111
 
73 112
 def lower(value):
74 113
     "Converts a string into all lowercase"
75 114
     return value.lower()
  115
+lower = stringfilter(lower)
76 116
 
77 117
 def make_list(value):
78 118
     """
79 119
     Returns the value turned into a list. For an integer, it's a list of
80 120
     digits. For a string, it's a list of characters.
81 121
     """
82  
-    return list(str(value))
  122
+    return list(value)
  123
+make_list = stringfilter(make_list)
83 124
 
84 125
 def slugify(value):
85 126
     "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
86 127
     value = re.sub('[^\w\s-]', '', value).strip().lower()
87 128
     return re.sub('[-\s]+', '-', value)
  129
+slugify = stringfilter(slugify)
88 130
 
89 131
 def stringformat(value, arg):
90 132
     """
@@ -96,13 +138,14 @@ def stringformat(value, arg):
96 138
     of Python string formatting
97 139
     """
98 140
     try:
99  
-        return ("%" + arg) % value
  141
+        return ("%" + str(arg)) % value
100 142
     except (ValueError, TypeError):
101 143
         return ""
102 144
 
103 145
 def title(value):
104 146
     "Converts a string into titlecase"
105 147
     return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
  148
+title = stringfilter(title)
106 149
 
107 150
 def truncatewords(value, arg):
108 151
     """
@@ -118,6 +161,7 @@ def truncatewords(value, arg):
118 161
     if not isinstance(value, basestring):
119 162
         value = str(value)
120 163
     return truncate_words(value, length)
  164
+truncatewords = stringfilter(truncatewords)
121 165
 
122 166
 def truncatewords_html(value, arg):
123 167
     """
@@ -133,10 +177,12 @@ def truncatewords_html(value, arg):
133 177
     if not isinstance(value, basestring):
134 178
         value = str(value)
135 179
     return truncate_html_words(value, length)
  180
+truncatewords_html = stringfilter(truncatewords_html)
136 181
 
137 182
 def upper(value):
138 183
     "Converts a string into all uppercase"
139 184
     return value.upper()
  185
+upper = stringfilter(upper)
140 186
 
141 187
 def urlencode(value):
142 188
     "Escapes a value for use in a URL"
@@ -144,11 +190,13 @@ def urlencode(value):
144 190
     if not isinstance(value, basestring):
145 191
         value = str(value)
146 192
     return urllib.quote(value)
  193
+urlencode = stringfilter(urlencode)
147 194
 
148 195
 def urlize(value):
149 196
     "Converts URLs in plain text into clickable links"
150 197
     from django.utils.html import urlize
151 198
     return urlize(value, nofollow=True)
  199
+urlize = stringfilter(urlize)
152 200
 
153 201
 def urlizetrunc(value, limit):
154 202
     """
@@ -159,10 +207,12 @@ def urlizetrunc(value, limit):
159 207
     """
160 208
     from django.utils.html import urlize
161 209
     return urlize(value, trim_url_limit=int(limit), nofollow=True)
  210
+urlizetrunc = stringfilter(urlizetrunc)
162 211
 
163 212
 def wordcount(value):
164 213
     "Returns the number of words"
165 214
     return len(value.split())
  215
+wordcount = stringfilter(wordcount)
166 216
 
167 217
 def wordwrap(value, arg):
168 218
     """
@@ -171,7 +221,8 @@ def wordwrap(value, arg):
171 221
     Argument: number of characters to wrap the text at.
172 222
     """
173 223
     from django.utils.text import wrap
174  
-    return wrap(str(value), int(arg))
  224
+    return wrap(value, int(arg))
  225
+wordwrap = stringfilter(wordwrap)
175 226
 
176 227
 def ljust(value, arg):
177 228
     """
@@ -179,7 +230,8 @@ def ljust(value, arg):
179 230
 
180 231
     Argument: field size
181 232
     """
182  
-    return str(value).ljust(int(arg))
  233
+    return value.ljust(int(arg))
  234
+ljust = stringfilter(ljust)
183 235
 
184 236
 def rjust(value, arg):
185 237
     """
@@ -187,15 +239,18 @@ def rjust(value, arg):
187 239
 
188 240
     Argument: field size
189 241
     """
190  
-    return str(value).rjust(int(arg))
  242
+    return value.rjust(int(arg))
  243
+rjust = stringfilter(rjust)
191 244
 
192 245
 def center(value, arg):
193 246
     "Centers the value in a field of a given width"
194  
-    return str(value).center(int(arg))
  247
+    return value.center(int(arg))
  248
+center = stringfilter(center)
195 249
 
196 250
 def cut(value, arg):
197 251
     "Removes all values of arg from the given string"
198 252
     return value.replace(arg, '')
  253
+cut = stringfilter(cut)
199 254
 
200 255
 ###################
201 256
 # HTML STRINGS    #
@@ -205,15 +260,18 @@ def escape(value):
205 260
     "Escapes a string's HTML"
206 261
     from django.utils.html import escape
207 262
     return escape(value)
  263
+escape = stringfilter(escape)
208 264
 
209 265
 def linebreaks(value):
210 266
     "Converts newlines into <p> and <br />s"
211 267
     from django.utils.html import linebreaks
212 268
     return linebreaks(value)
  269
+linebreaks = stringfilter(linebreaks)
213 270
 
214 271
 def linebreaksbr(value):
215 272
     "Converts newlines into <br />s"
216 273
     return value.replace('\n', '<br />')
  274
+linebreaksbr = stringfilter(linebreaksbr)
217 275
 
218 276
 def removetags(value, tags):
219 277
     "Removes a space separated list of [X]HTML tags from the output"
@@ -224,13 +282,13 @@ def removetags(value, tags):
224 282
     value = starttag_re.sub('', value)
225 283
     value = endtag_re.sub('', value)
226 284
     return value
  285
+removetags = stringfilter(removetags)
227 286
 
228 287
 def striptags(value):
229 288
     "Strips all [X]HTML tags"
230 289
     from django.utils.html import strip_tags
231  
-    if not isinstance(value, basestring):
232  
-        value = str(value)
233 290
     return strip_tags(value)
  291
+striptags = stringfilter(striptags)
234 292
 
235 293
 ###################
236 294
 # LISTS           #
@@ -265,7 +323,7 @@ def first(value):
265 323
 def join(value, arg):
266 324
     "Joins a list with a string, like Python's ``str.join(list)``"
267 325
     try:
268  
-        return arg.join(map(str, value))
  326
+        return arg.join(map(smart_string, value))
269 327
     except AttributeError: # fail silently but nicely
270 328
         return value
271 329
 
10  docs/templates_python.txt
@@ -654,6 +654,16 @@ decorator instead::
654 654
 If you leave off the ``name`` argument, as in the second example above, Django
655 655
 will use the function's name as the filter name.
656 656
 
  657
+Template filters which expect strings
  658
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  659
+If you are writing a template filter which only expects a string as the first
  660
+argument, you should use the included decorator ``to_str`` which will convert
  661
+an object to it's string value before being passed to your function::
  662
+
  663
+    def lower(value):
  664
+    	return value.lower()
  665
+    lower = template.to_str(lower)
  666
+
657 667
 Writing custom template tags
658 668
 ----------------------------
659 669
 
48  tests/regressiontests/defaultfilters/tests.py
@@ -388,7 +388,53 @@
388 388
 >>> phone2numeric('0800 flowers')
389 389
 '0800 3569377'
390 390
 
391  
-
  391
+# Filters shouldn't break if passed non-strings
  392
+>>> addslashes(123)
  393
+'123'
  394
+>>> linenumbers(123)
  395
+'1. 123'
  396
+>>> lower(123)
  397
+'123'
  398
+>>> make_list(123)
  399
+['1', '2', '3']
  400
+>>> slugify(123)
  401
+'123'
  402
+>>> title(123)
  403
+'123'
  404
+>>> truncatewords(123, 2)
  405
+'123'
  406
+>>> upper(123)
  407
+'123'
  408
+>>> urlencode(123)
  409
+'123'
  410
+>>> urlize(123)
  411
+'123'
  412
+>>> urlizetrunc(123, 1)
  413
+'123'
  414
+>>> wordcount(123)
  415
+1
  416
+>>> wordwrap(123, 2)
  417
+'123'
  418
+>>> ljust('123', 4)
  419
+'123 '
  420
+>>> rjust('123', 4)
  421
+' 123'
  422
+>>> center('123', 5)
  423
+' 123 '
  424
+>>> center('123', 6)
  425
+' 123  '
  426
+>>> cut(123, '2')
  427
+'13'
  428
+>>> escape(123)
  429
+'123'
  430
+>>> linebreaks(123)
  431
+'<p>123</p>'
  432
+>>> linebreaksbr(123)
  433
+'123'
  434
+>>> removetags(123, 'a')
  435
+'123'
  436
+>>> striptags(123)
  437
+'123'
392 438
 
393 439
 """
394 440
 

0 notes on commit 36512d5

Please sign in to comment.
Something went wrong with that request. Please try again.