Code

Ticket #1105: improved_simple_tag.r7818.diff

File improved_simple_tag.r7818.diff, 11.7 KB (added by julien, 6 years ago)

Replaced "takes_block" by "takes_nodelist", and "block_nodelist" by "nodelist". Amended the doc and tests as well.

Line 
1Index: django/django/template/__init__.py
2===================================================================
3--- django/django/template/__init__.py  (revision 7818)
4+++ django/django/template/__init__.py  (working copy)
5@@ -792,7 +792,7 @@
6         else:
7             return force_unicode(output)
8 
9-def generic_tag_compiler(params, defaults, name, node_class, parser, token):
10+def generic_tag_compiler(params, defaults, name, node_class, parser, token, takes_context=False, takes_nodelist=False):
11     "Returns a template.Node subclass."
12     bits = token.split_contents()[1:]
13     bmax = len(params)
14@@ -804,6 +804,12 @@
15         else:
16             message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
17         raise TemplateSyntaxError(message)
18+    if takes_context:
19+        node_class = curry(node_class, takes_context=takes_context)
20+    if takes_nodelist:
21+        nodelist = parser.parse(('end%s' % name,))
22+        parser.delete_first_token()
23+        node_class = curry(node_class, nodelist=nodelist)
24     return node_class(bits)
25 
26 class Library(object):
27@@ -859,21 +865,61 @@
28         self.filters[getattr(func, "_decorated_function", func).__name__] = func
29         return func
30 
31-    def simple_tag(self,func):
32-        params, xx, xxx, defaults = getargspec(func)
33+    def simple_tag(self, compile_function=None, takes_context=None, takes_nodelist=None):
34+        def dec(func):
35+            params, xx, xxx, defaults = getargspec(func)
36+            if takes_context and takes_nodelist:
37+                if params[0] == 'context' and params[1] == 'nodelist':
38+                    params = params[2:]
39+                else:
40+                    raise TemplateSyntaxError("Any tag function decorated both with takes_context=True and with takes_nodelist=True must have a first argument of 'context', and a second argument of 'nodelist'")
41+            elif takes_nodelist:
42+                if params[0] == 'nodelist':
43+                    params = params[1:]
44+                else:
45+                    raise TemplateSyntaxError("Any tag function decorated with takes_nodelist=True must have a first argument of 'nodelist'")
46+            elif takes_context:
47+                if params[0] == 'context':
48+                    params = params[1:]
49+                else:
50+                    raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
51 
52-        class SimpleNode(Node):
53-            def __init__(self, vars_to_resolve):
54-                self.vars_to_resolve = map(Variable, vars_to_resolve)
55+            class SimpleNode(Node):
56+                def __init__(self, vars_to_resolve, takes_context=False, nodelist=None):
57+                    self.vars_to_resolve = map(Variable, vars_to_resolve)
58+                    self.takes_context = takes_context
59+                    if nodelist is not None:
60+                        # Only save the 'nodelist' attribute if it's not None, so that it is picked by the Node.get_nodes_by_type() method.
61+                        self.nodelist = nodelist
62 
63-            def render(self, context):
64-                resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
65-                return func(*resolved_vars)
66+                def render(self, context):
67+                    resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
68+                    if self.takes_context and hasattr(self, 'nodelist'):
69+                        func_args = [context, self.nodelist] + resolved_vars
70+                    elif hasattr(self, 'nodelist'):
71+                        func_args = [self.nodelist] + resolved_vars
72+                    elif self.takes_context:
73+                        func_args = [context] + resolved_vars
74+                    else:
75+                        func_args = resolved_vars
76+                    return func(*func_args)
77 
78-        compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode)
79-        compile_func.__doc__ = func.__doc__
80-        self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
81-        return func
82+            compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode, takes_nodelist=takes_nodelist, takes_context=takes_context)
83+            compile_func.__doc__ = func.__doc__
84+            self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
85+            return func
86+       
87+        if takes_context is not None or takes_nodelist is not None:
88+            # Examples: @register.simple_tag(takes_context=True) or @register.simple_tag(takes_context=True, takes_nodelist=True)
89+            return dec
90+        elif compile_function is None:
91+            # @register.simple_tag()
92+            return dec
93+        elif callable(compile_function):
94+            # @register.simple_tag
95+            return dec(compile_function)
96+        else:
97+            raise TemplateSyntaxError("Incorrect parameters for the simple_tag decorator.")
98 
99     def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
100         def dec(func):
101Index: django/tests/regressiontests/templates/tests.py
102===================================================================
103--- django/tests/regressiontests/templates/tests.py     (revision 7818)
104+++ django/tests/regressiontests/templates/tests.py     (working copy)
105@@ -17,6 +17,7 @@
106 from django.utils.safestring import mark_safe
107 from django.utils.tzinfo import LocalTimezone
108 
109+from decorators import DecoratorsTest
110 from unicode import unicode_tests
111 from context import context_tests
112 
113Index: django/tests/regressiontests/templates/decorators.py
114===================================================================
115--- django/tests/regressiontests/templates/decorators.py        (revision 0)
116+++ django/tests/regressiontests/templates/decorators.py        (revision 0)
117@@ -0,0 +1,73 @@
118+from unittest import TestCase
119+from unittest import TestCase
120+from sys import version_info
121+
122+from django import template
123+
124+register = template.Library()
125+
126+# Very simple tag, with no parameters.
127+def a_simple_tag_without_parameters(arg):
128+    """Expected __doc__"""
129+    return "Expected result"
130+a_simple_tag_without_parameters.anything = "Expected __dict__"
131+
132+# Tag that takes the context.
133+def a_simple_tag_with_context(context, arg):
134+    """Expected __doc__"""
135+    return "Expected result"
136+a_simple_tag_with_context.anything = "Expected __dict__"
137+
138+# Tag that takes the inner block's node list.
139+def a_simple_tag_with_nodelist(nodelist, arg):
140+    """Expected __doc__"""
141+    return "Expected result"
142+a_simple_tag_with_nodelist.anything = "Expected __dict__"
143+
144+# Tag that takes both the context and the inner block's node list.
145+def a_simple_tag_with_context_and_nodelist(context, nodelist, arg):
146+    """Expected __doc__"""
147+    return "Expected result"
148+a_simple_tag_with_context_and_nodelist.anything = "Expected __dict__"
149+
150+# Tag that *wants* to take both the context and the inner block's node list, but that has arguments in wrong order.
151+def a_simple_tag_with_context_and_nodelist_wrong_order(nodelist, context, arg):
152+    """Expected __doc__"""
153+    return "Expected result"
154+a_simple_tag_with_context_and_nodelist_wrong_order.anything = "Expected __dict__"
155+
156+
157+
158+
159+class DecoratorsTest(TestCase):
160+    def verify_decorator(self, decorator, func_name):
161+        # Only check __name__ on Python 2.4 or later since __name__ can't be
162+        # assigned to in earlier Python versions.
163+        if version_info[0] >= 2 and version_info[1] >= 4:
164+            self.assertEquals(decorator.__name__, func_name)
165+        self.assertEquals(decorator.__doc__, 'Expected __doc__')
166+        self.assertEquals(decorator.__dict__['anything'], 'Expected __dict__')
167+
168+    def test_simple_tag(self):
169+        # Test that the decorators preserve the decorated function's docstring, name and attributes.
170+        decorator = register.simple_tag(a_simple_tag_without_parameters)
171+        self.verify_decorator(decorator, 'a_simple_tag_without_parameters')
172+       
173+        decorator = register.simple_tag(takes_context=True)(a_simple_tag_with_context)
174+        self.verify_decorator(decorator, 'a_simple_tag_with_context')
175+       
176+        decorator = register.simple_tag(takes_nodelist=True)(a_simple_tag_with_nodelist)
177+        self.verify_decorator(decorator, 'a_simple_tag_with_nodelist')
178+       
179+        decorator = register.simple_tag(takes_context=True, takes_nodelist=True)(a_simple_tag_with_context_and_nodelist)
180+        self.verify_decorator(decorator, 'a_simple_tag_with_context_and_nodelist')
181+       
182+        # Now test that 'context' and 'nodelist' arguments and their order are correct.
183+        decorator = register.simple_tag(takes_context=True)
184+        self.assertRaises(template.TemplateSyntaxError, decorator, a_simple_tag_without_parameters)
185+       
186+        decorator = register.simple_tag(takes_nodelist=True)
187+        self.assertRaises(template.TemplateSyntaxError, decorator, a_simple_tag_without_parameters)
188+       
189+        decorator = register.simple_tag(takes_nodelist=True, takes_context=True)
190+        self.assertRaises(template.TemplateSyntaxError, decorator, a_simple_tag_with_context_and_nodelist_wrong_order)
191Index: django/docs/templates_python.txt
192===================================================================
193--- django/docs/templates_python.txt    (revision 7818)
194+++ django/docs/templates_python.txt    (working copy)
195@@ -1181,10 +1181,53 @@
196     * If the argument was a template variable, our function is passed the
197       current value of the variable, not the variable itself.
198 
199-When your template tag does not need access to the current context, writing a
200-function to work with the input values and using the ``simple_tag`` helper is
201-the easiest way to create a new tag.
202+If your template tag needs to access the current context, you can use the
203+``takes_context`` option as follows::
204 
205+    # The first argument *must* be called "context" here.
206+    def current_time(context, format_string):
207+        timezone = context['timezone']
208+        ...
209+
210+    register.simple_tag(takes_context=True)(current_time)
211+
212+You can also use the decorator syntax if running in Python 2.4::
213+
214+    @register.simple_tag(takes_context=True)
215+    def current_time(context, format_string):
216+        ...
217+
218+For more information on how the ``takes_context`` option works, see the section
219+on `inclusion tags`_.
220+
221+If your template is a simple block tag, you can use the ``takes_nodelist`` option as
222+follows::
223+
224+    # The first argument *must* be called "nodelist".
225+    def my_bock_tag(nodelist, an_argument):
226+        ...
227+    register.simple_tag(takes_nodelist=True)(my_bock_tag)
228+
229+In the above example, ``nodelist`` is a list of all nodes between
230+``{% my_bock_tag %}`` and ``{% endmy_bock_tag %}``, not counting
231+``{% my_bock_tag %}`` and ``{% endmy_bock_tag %}`` themselves.
232+
233+It is also possible to use both ``takes_context`` and ``takes_nodelist`` at the
234+same time. For example::
235+
236+    # The first argument *must* be called "context" and the second one "nodelist".
237+    def my_bock_tag(context, nodelist, an_argument):
238+        timezone = context['timezone']
239+        content = nodelist.render(context)
240+        ...
241+    register.simple_tag(takes_context=True, takes_nodelist=True)(my_bock_tag)
242+
243+If you need to create a more complex block tag, refer to the section on
244+`parsing until another block tag`_.
245+
246+_inclusion tags: #inclusion-tags
247+_block tags: #parsing-until-another-block-tag
248+
249 Inclusion tags
250 ~~~~~~~~~~~~~~
251