Ticket #20434: patch1.patch

File patch1.patch, 35.1 KB (added by jonathanslenders, 11 years ago)

creation of the generic TemplateTag, the Grammar, and a little refactoring to make inclusion_tag, assignment_tag and simple_tag use this one behind. It also includes all the unit tests for this.

  • AUTHORS

    commit f5255b1672a60023c82a035f25cc679dc299fed2
    Author: Jonathan Slenders <jonathan@slenders.be>
    Date:   Tue Jun 4 17:35:58 2013 +0200
    
        - Introduction of the TemplateTag class and Grammar class.
        - SimpleTag, InclusionTag and AssignmentTag automatically become a TemplateTag class
        - LibraryGrammar for extraction of the grammar of a template tag library.
        - Also added unit tests.
    
    diff --git a/AUTHORS b/AUTHORS
    index 80f2779..7c4e012 100644
    a b answer newbie questions, and generally made Django that much better:  
    634634    Gasper Zejn <zejn@kiberpipa.org>
    635635    Jarek Zgoda <jarek.zgoda@gmail.com>
    636636    Cheng Zhang
     637    Jonathan Slenders
    637638
    638639A big THANK YOU goes to:
    639640
  • django/template/__init__.py

    diff --git a/django/template/__init__.py b/django/template/__init__.py
    index ca1bd49..0e55d62 100644
    a b from django.template.base import (Context, FilterExpression, Lexer, Node,  
    7070
    7171# Compiling templates
    7272from django.template.base import (compile_string, resolve_variable,
    73     unescape_string_literal, generic_tag_compiler)
     73    unescape_string_literal)
    7474
    7575# Library management
    7676from django.template.base import (Library, add_to_builtins, builtins,
    7777    get_library, get_templatetags_modules, get_text_list, import_library,
    7878    libraries)
    7979
     80# Class based templates
     81from django.template.generic import Grammar, UnknownGrammar, GrammarException, TemplateTag
     82
    8083__all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
  • django/template/base.py

    diff --git a/django/template/base.py b/django/template/base.py
    index dc6b0c3..2de407c 100644
    a b  
    11from __future__ import absolute_import, unicode_literals
    22
     3import inspect
    34import re
    45from functools import partial
    56from inspect import getargspec
    def parse_bits(parser, bits, params, varargs, varkw, defaults,  
    10121013            (name, ", ".join(["'%s'" % p for p in unhandled_params])))
    10131014    return args, kwargs
    10141015
    1015 def generic_tag_compiler(parser, token, params, varargs, varkw, defaults,
    1016                          name, takes_context, node_class):
     1016def _get_resolved_arguments(args, kwargs, takes_context, context):
    10171017    """
    1018     Returns a template.Node subclass.
     1018    A helper for tags such as SimpleTag, InclusionTag and AssignmentTag.
     1019    Manages the positional and keyword arguments to be passed to the decorated
     1020    function.
    10191021    """
    1020     bits = token.split_contents()[1:]
    1021     args, kwargs = parse_bits(parser, bits, params, varargs, varkw,
    1022                               defaults, takes_context, name)
    1023     return node_class(takes_context, args, kwargs)
    1024 
    1025 class TagHelperNode(Node):
    1026     """
    1027     Base class for tag helper nodes such as SimpleNode, InclusionNode and
    1028     AssignmentNode. Manages the positional and keyword arguments to be passed
    1029     to the decorated function.
    1030     """
    1031 
    1032     def __init__(self, takes_context, args, kwargs):
    1033         self.takes_context = takes_context
    1034         self.args = args
    1035         self.kwargs = kwargs
    1036 
    1037     def get_resolved_arguments(self, context):
    1038         resolved_args = [var.resolve(context) for var in self.args]
    1039         if self.takes_context:
    1040             resolved_args = [context] + resolved_args
    1041         resolved_kwargs = dict((k, v.resolve(context))
    1042                                 for k, v in self.kwargs.items())
    1043         return resolved_args, resolved_kwargs
     1022    resolved_args = [var.resolve(context) for var in args]
     1023    if takes_context:
     1024        resolved_args = [context] + resolved_args
     1025    resolved_kwargs = dict((k, v.resolve(context))
     1026                            for k, v in kwargs.items())
     1027    return resolved_args, resolved_kwargs
    10441028
    10451029class Library(object):
    10461030    def __init__(self):
    class Library(object):  
    10481032        self.tags = {}
    10491033
    10501034    def tag(self, name=None, compile_function=None):
     1035        from django.template.generic import TemplateTag, UnknownGrammar
    10511036        if name is None and compile_function is None:
    10521037            # @register.tag()
    10531038            return self.tag_function
    10541039        elif name is not None and compile_function is None:
    10551040            if callable(name):
    10561041                # @register.tag
    1057                 return self.tag_function(name)
     1042                if inspect.isclass(name) and issubclass(name, TemplateTag):
     1043                    # When a TemplateTag instance is registered, create a
     1044                    # compile_funcion from the class. It's nicer to handle this
     1045                    # separately than handling this class as any other
     1046                    # callable, and ending with the (parser, token) in the
     1047                    # constructor of TemplateTag.
     1048                    fname, func = name.as_compile_funcion()
     1049                    self.tag(name=fname, compile_function=func)
     1050                    return name
     1051                else:
     1052                    return self.tag_function(name)
    10581053            else:
    10591054                # @register.tag('somename') or @register.tag(name='somename')
    10601055                def dec(func):
    10611056                    return self.tag(name, func)
    10621057                return dec
    10631058        elif name is not None and compile_function is not None:
    1064             # register.tag('somename', somefunc)
     1059            # Make sure that every compile_function has a grammar instance
     1060            # attached.
     1061            if not hasattr(compile_function, 'grammar'):
     1062                compile_function.grammar = UnknownGrammar(name)
     1063
    10651064            self.tags[name] = compile_function
    10661065            return compile_function
    10671066        else:
    class Library(object):  
    10691068                "Library.tag: (%r, %r)", (name, compile_function))
    10701069
    10711070    def tag_function(self, func):
    1072         self.tags[getattr(func, "_decorated_function", func).__name__] = func
     1071        name = getattr(func, "_decorated_function", func).__name__
     1072        self.tag(name=name, compile_function=func)
    10731073        return func
    10741074
    10751075    def filter(self, name=None, filter_func=None, **flags):
    class Library(object):  
    11111111        return self.filter(name, func, **flags)
    11121112
    11131113    def simple_tag(self, func=None, takes_context=None, name=None):
     1114        from django.template.generic import TemplateTag, Grammar
    11141115        def dec(func):
    11151116            params, varargs, varkw, defaults = getargspec(func)
    11161117
    1117             class SimpleNode(TagHelperNode):
     1118            function_name = (name or
     1119                getattr(func, '_decorated_function', func).__name__)
     1120
     1121            class _SimpleTag(TemplateTag):
     1122                __doc__ = func.__doc__
     1123                grammar = Grammar(function_name)
     1124
     1125                def __init__(self, parser, parse_result):
     1126                    self.args, self.kwargs = parse_bits(parser, parse_result.arguments,
     1127                              params, varargs, varkw, defaults, takes_context, function_name)
    11181128
    11191129                def render(self, context):
    1120                     resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
     1130                    resolved_args, resolved_kwargs = _get_resolved_arguments(self.args,
     1131                              self.kwargs, takes_context, context)
    11211132                    return func(*resolved_args, **resolved_kwargs)
    11221133
    1123             function_name = (name or
    1124                 getattr(func, '_decorated_function', func).__name__)
    1125             compile_func = partial(generic_tag_compiler,
    1126                 params=params, varargs=varargs, varkw=varkw,
    1127                 defaults=defaults, name=function_name,
    1128                 takes_context=takes_context, node_class=SimpleNode)
    1129             compile_func.__doc__ = func.__doc__
    1130             self.tag(function_name, compile_func)
     1134            self.tag(_SimpleTag)
    11311135            return func
    11321136
    11331137        if func is None:
    class Library(object):  
    11401144            raise TemplateSyntaxError("Invalid arguments provided to simple_tag")
    11411145
    11421146    def assignment_tag(self, func=None, takes_context=None, name=None):
     1147        from django.template.generic import TemplateTag, Grammar
    11431148        def dec(func):
    11441149            params, varargs, varkw, defaults = getargspec(func)
    11451150
    1146             class AssignmentNode(TagHelperNode):
    1147                 def __init__(self, takes_context, args, kwargs, target_var):
    1148                     super(AssignmentNode, self).__init__(takes_context, args, kwargs)
    1149                     self.target_var = target_var
     1151            function_name = (name or
     1152                getattr(func, '_decorated_function', func).__name__)
     1153
     1154            class _AssignmentTag(TemplateTag):
     1155                grammar = Grammar(function_name)
     1156                __doc__ = func.__doc__
     1157
     1158                def __init__(self, parser, parse_result):
     1159                    bits = parse_result.arguments
     1160                    if len(bits) < 2 or bits[-2] != 'as':
     1161                        raise TemplateSyntaxError(
     1162                            "'%s' tag takes at least 2 arguments and the "
     1163                            "second last argument must be 'as'" % function_name)
     1164                    self.target_var = bits[-1]
     1165                    self.args, self.kwargs = parse_bits(parser, bits[:-2],
     1166                              params, varargs, varkw, defaults, takes_context, function_name)
    11501167
    11511168                def render(self, context):
    1152                     resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
     1169                    resolved_args, resolved_kwargs = _get_resolved_arguments(self.args,
     1170                                self.kwargs, takes_context, context)
    11531171                    context[self.target_var] = func(*resolved_args, **resolved_kwargs)
    11541172                    return ''
    11551173
    1156             function_name = (name or
    1157                 getattr(func, '_decorated_function', func).__name__)
    1158 
    1159             def compile_func(parser, token):
    1160                 bits = token.split_contents()[1:]
    1161                 if len(bits) < 2 or bits[-2] != 'as':
    1162                     raise TemplateSyntaxError(
    1163                         "'%s' tag takes at least 2 arguments and the "
    1164                         "second last argument must be 'as'" % function_name)
    1165                 target_var = bits[-1]
    1166                 bits = bits[:-2]
    1167                 args, kwargs = parse_bits(parser, bits, params,
    1168                     varargs, varkw, defaults, takes_context, function_name)
    1169                 return AssignmentNode(takes_context, args, kwargs, target_var)
    1170 
    1171             compile_func.__doc__ = func.__doc__
    1172             self.tag(function_name, compile_func)
     1174            self.tag(_AssignmentTag)
    11731175            return func
    11741176
    11751177        if func is None:
    class Library(object):  
    11821184            raise TemplateSyntaxError("Invalid arguments provided to assignment_tag")
    11831185
    11841186    def inclusion_tag(self, file_name, context_class=Context, takes_context=False, name=None):
     1187        from django.template.generic import TemplateTag, Grammar
    11851188        def dec(func):
    11861189            params, varargs, varkw, defaults = getargspec(func)
    11871190
    1188             class InclusionNode(TagHelperNode):
     1191            function_name = (name or
     1192                getattr(func, '_decorated_function', func).__name__)
     1193
     1194            class _InclusionTag(TemplateTag):
     1195                grammar = Grammar(function_name)
     1196                __doc__ = func.__doc__
     1197
     1198                def __init__(self, parser, parse_result):
     1199                    self.args, self.kwargs = parse_bits(parser, parse_result.arguments,
     1200                              params, varargs, varkw, defaults, takes_context, function_name)
    11891201
    11901202                def render(self, context):
    1191                     resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
     1203                    resolved_args, resolved_kwargs = _get_resolved_arguments(self.args,
     1204                                self.kwargs, takes_context, context)
     1205
    11921206                    _dict = func(*resolved_args, **resolved_kwargs)
    11931207
    1194                     if not getattr(self, 'nodelist', False):
     1208                    if not getattr(self, '_nodelist', False):
    11951209                        from django.template.loader import get_template, select_template
    11961210                        if isinstance(file_name, Template):
    11971211                            t = file_name
    class Library(object):  
    11991213                            t = select_template(file_name)
    12001214                        else:
    12011215                            t = get_template(file_name)
    1202                         self.nodelist = t.nodelist
     1216                        self._nodelist = t.nodelist
    12031217                    new_context = context_class(_dict, **{
    12041218                        'autoescape': context.autoescape,
    12051219                        'current_app': context.current_app,
    class Library(object):  
    12131227                    csrf_token = context.get('csrf_token', None)
    12141228                    if csrf_token is not None:
    12151229                        new_context['csrf_token'] = csrf_token
    1216                     return self.nodelist.render(new_context)
     1230                    return self._nodelist.render(new_context)
    12171231
    1218             function_name = (name or
    1219                 getattr(func, '_decorated_function', func).__name__)
    1220             compile_func = partial(generic_tag_compiler,
    1221                 params=params, varargs=varargs, varkw=varkw,
    1222                 defaults=defaults, name=function_name,
    1223                 takes_context=takes_context, node_class=InclusionNode)
    1224             compile_func.__doc__ = func.__doc__
    1225             self.tag(function_name, compile_func)
     1232            self.tag(_InclusionTag)
    12261233            return func
    12271234        return dec
    12281235
     1236    def get_grammar(self):
     1237        return LibraryGrammar(self)
     1238
     1239
     1240class LibraryGrammar(object):
     1241    def __init__(self, library=None):
     1242        self._tags = []
     1243
     1244        if library:
     1245            self.add_library(library)
     1246
     1247    def add_library(self, library):
     1248        for name, compile_func in library.tags.items():
     1249            grammar = compile_func.grammar
     1250            self._tags.append((grammar.first_tagname, grammar.as_string()))
     1251
     1252        self._tags = sorted(self._tags, key=lambda i:i[0])
     1253
     1254    def as_string(self, style=None):
     1255        """
     1256        Return a BNF like notation.
     1257        """
     1258        max_length = max(len(i[0]) for i in self._tags)
     1259        format = '%%-%ss ::= %%s' % max_length
     1260
     1261        return '\n'.join(format % (g[0], g[1]) for g in self._tags)
     1262
     1263    @classmethod
     1264    def from_builtins(cls):
     1265        library_grammar = cls()
     1266        for b in builtins:
     1267            library_grammar.add_library(b)
     1268        return library_grammar
     1269
     1270
    12291271def is_library_missing(name):
    12301272    """Check if library that failed to load cannot be found under any
    12311273    templatetags directory or does exist but fails to import.
  • new file django/template/generic.py

    diff --git a/django/template/generic.py b/django/template/generic.py
    new file mode 100644
    index 0000000..11964e4
    - +  
     1from django.template.base import Node, NodeList, TemplateSyntaxError
     2import re
     3
     4
     5class GrammarException(Exception):
     6    pass
     7
     8
     9class Grammar(object):
     10    """
     11    Generic template tag grammar.
     12
     13    A grammar is defined by a space separated list of tagnames, optionally with
     14    a modifier to indicate how many times this tag can appear:
     15    - *: zero or more times;
     16    - +: one or more times;
     17    - ?: zero or one time;
     18    - (no suffix): exactly one time.
     19
     20    For instance: 'if elif* else? endif'
     21
     22    The constraints are:
     23    - The first and last template tags cannot have any modifier.
     24    - A tagname should not appear more than once in a single grammar.
     25
     26    This simple grammar does not automatically parse or validate template tag
     27    parameters.
     28    """
     29    tagname_re = re.compile(r"^[a-zA-Z0-9_]+$")
     30
     31    class ParseResult(object):
     32        def __init__(self, parser, parts):
     33            self.parser = parser
     34            self.parts = parts
     35
     36            self.nodelist = NodeList(node for p in parts for node in p.nodelist)
     37
     38        @property
     39        def tagname(self):
     40            return self.parts[0].name
     41
     42        @property
     43        def arguments(self):
     44            return self.parts[0].arguments
     45
     46    class TemplateTagPart(object):
     47        def __init__(self, token, name, arguments, nodelist):
     48            self.token = token
     49            self.name = name
     50            self.arguments = arguments
     51            self.nodelist = nodelist
     52
     53    def __init__(self, grammar, _split_contents=True):
     54        # XXX _split_contents is for internal use only.
     55        if not grammar:
     56            raise GrammarException('No template tags given.')
     57
     58        result = []
     59        for tag in grammar.split():
     60            if tag.endswith('*') or tag.endswith('+') or tag.endswith('?'):
     61                tagname, flag = tag[:-1], tag[-1]
     62            else:
     63                tagname, flag = tag, None
     64
     65            if not self.tagname_re.match(tagname):
     66                raise GrammarException('%s is not a valid template tag name' % tagname)
     67
     68            if tagname in result:
     69                raise GrammarException('The template tag %s is defined more than once.' % tagname)
     70
     71            result.append(tagname)
     72            result.append(flag)
     73
     74        # Validate grammar
     75        if not result:
     76            raise GrammarException('No template tags given.')
     77        if result[1] != None:
     78            raise GrammarException('The first template tag should not repeat.')
     79        if result[-1] != None:
     80            raise GrammarException('The last template tag should not repeat.')
     81
     82        self._split_contents = _split_contents
     83        self._grammar = result
     84        self._grammar_string = grammar
     85
     86        self.first_tagname = result[0]
     87
     88    @property
     89    def is_simple(self):
     90        """ True when this grammar consists of a single tag only. """
     91        return len(self._grammar) == 2 and self._grammar[1] == None
     92
     93    def as_string(self, style=None):
     94        """
     95        Return a BNF like notation for this grammar.
     96        """
     97        return self._grammar_string
     98
     99    def parse(self, parser, token):
     100        """
     101        Apply this generic parser, on a token input, and return a ParseResult.
     102        """
     103        parts = []
     104        _grammar = self._grammar[:]
     105
     106        while _grammar:
     107            if token.contents.startswith(_grammar[0]):
     108                name = _grammar[0]
     109                current_token = token
     110                arguments = self.parse_arguments(name, token)
     111
     112                if len(_grammar) == 2:
     113                    # Last tag
     114                    nodelist = NodeList()
     115                    _grammar = []
     116                else:
     117                    if _grammar[1] == '+':
     118                        _grammar[1] = '*'
     119
     120                    elif _grammar[1] in (None, '?'):
     121                        _grammar = _grammar[2:]
     122
     123                    nodelist = parser.parse(_grammar[::2])
     124                    token = parser.next_token()
     125
     126                parts.append(self.TemplateTagPart(current_token, name, arguments, nodelist))
     127
     128            elif _grammar[1] in ('?', '*'):
     129                # No match, pop grammar, and try again.
     130                _grammar = _grammar[2:]
     131
     132            else:
     133                # No match. Invalid template.
     134                raise TemplateSyntaxError('Expected "%s" template tag, but found "%s" instead.' %
     135                                (_grammar[0], token.contents))
     136
     137        return self.ParseResult(parser, parts)
     138
     139    def parse_arguments(self, tagname, token):
     140        if self._split_contents:
     141            return token.split_contents()[1:]
     142        try:
     143            return token.contents.split(None, 1)[1]
     144        except IndexError:
     145            return ''
     146
     147
     148class UnknownGrammar(Grammar):
     149    # Grammar-compatible class which will be attached to compile_functions that
     150    # don't have a grammar yet.
     151    def __init__(self, tagname):
     152        self.first_tagname = tagname
     153        self._grammar_string = '<Unknown Grammar>'
     154
     155    def parse(self, parser, token):
     156        raise NotImplementedError
     157
     158    def as_string(self):
     159        return self._grammar_string
     160
     161
     162class TemplateTag(Node):
     163    """
     164    Generic template tag.
     165    """
     166    grammar = None
     167
     168    @classmethod
     169    def as_compile_funcion(cls):
     170        if not cls.grammar:
     171            raise TemplateSyntaxError('TemplateTag has no valid grammar.')
     172        def compile_function(parser, token):
     173            return cls.parse(parser, token)
     174        compile_function.grammar = cls.grammar
     175        compile_function.__name__ = str(cls.grammar.first_tagname)
     176        compile_function.__doc__ = cls.__doc__
     177        return cls.grammar.first_tagname, compile_function
     178
     179    @classmethod
     180    def parse(cls, parser, token):
     181        """ Turn parser/token into Node """
     182        parse_result = cls.grammar.parse(parser, token)
     183        node = cls.handle_parse_result(parser, parse_result)
     184        node.parse_result = parse_result
     185        return node
     186
     187    @classmethod
     188    def handle_parse_result(cls, parser, parse_result):
     189        """ Create Node instance from parse result. """
     190        node = cls(parser, parse_result)
     191        return node
     192
     193    def __init__(self, parser, parse_result):
     194        pass
     195
     196    def __iter__(self):
     197        for node in self.nodelist:
     198            yield node
     199
     200    @property
     201    def nodelist(self):
     202        return self.parse_result.nodelist
     203
     204    def render(self, context):
     205        return self.nodelist.render(context)
  • new file tests/template_tests/templatetags/class_based.py

    diff --git a/tests/template_tests/templatetags/class_based.py b/tests/template_tests/templatetags/class_based.py
    new file mode 100644
    index 0000000..f0543f5
    - +  
     1import operator
     2
     3from django import template
     4from django.template.defaultfilters import stringfilter
     5from django.template.loader import get_template
     6from django.template.generic import TemplateTag, Grammar
     7from django.utils import six
     8
     9register = template.Library()
     10
     11
     12
     13@register.tag
     14class OneTag(TemplateTag):
     15    """
     16    A simple templatetag which does nothing actually.
     17    {% cb_one_tag %}
     18    """
     19    grammar = Grammar('cb_one_tag')
     20
     21@register.tag
     22class TwoTags(TemplateTag):
     23    """
     24    A templatetag which just renders the containing nodes.
     25    {% cb_two_tags %} ... {% end_cb_two_tags %}
     26    """
     27    grammar = Grammar('cb_two_tags end_cb_two_tags')
     28
     29@register.tag
     30class ToUpper(TemplateTag):
     31    """
     32    A template tags which converts the inner noders to uppercase
     33    after rendering.
     34    {% cb_to_upper %} ... {% end_cb_to_upper %}
     35    """
     36    grammar = Grammar('cb_to_upper end_cb_to_upper')
     37
     38    def render(self, context):
     39        return self.nodelist.render(context).upper()
     40
     41@register.tag
     42class WithMiddleTag(TemplateTag):
     43    """
     44    A template tags pair which has a middle tag.
     45    {% cb_with_middle %} ... {% middle %} ... {% end_cb_with_middle %}
     46    """
     47    grammar = Grammar('cb_with_middle middle end_cb_with_middle')
     48
     49@register.tag
     50class RenderSecondNode(TemplateTag):
     51    """
     52    A template tags which converts the inner noders to uppercase
     53    after rendering.
     54    {% cb_render_second %} ... {% middle %} ... {% end_cb_render_second %}
     55    """
     56    grammar = Grammar('cb_render_second middle end_cb_render_second')
     57
     58    def render(self, context):
     59        return self.parse_result.parts[1].nodelist.render(context)
     60
     61@register.tag
     62class RenderSecondNode2(TemplateTag):
     63    """
     64    Another approach.
     65    {% cb_render_second2 %} ... {% middle %} ... {% end_cb_render_second2 %}
     66    """
     67    grammar = Grammar('cb_render_second2 middle end_cb_render_second2')
     68
     69    def __init__(self, parser, parse_result):
     70        self.second_nodelist = parse_result.parts[1].nodelist
     71
     72    def render(self, context):
     73        return self.second_nodelist.render(context)
     74
     75@register.tag
     76class OptionalMiddle(TemplateTag):
     77    """
     78    The middle tag optional. Just renders everything.
     79    {% cb_optional_middle %} ... {% middle %} ... {% end_cb_optional_middle %}
     80    """
     81    grammar = Grammar('cb_optional_middle middle? end_cb_optional_middle')
     82
     83@register.tag
     84class RepeatingMiddle(TemplateTag):
     85    """
     86    The middle tag repeatable 0..n times.
     87    """
     88    grammar = Grammar('cb_repeating_middle middle* end_cb_repeating_middle')
     89
     90@register.tag
     91class OneOrMoreMidleMiddle(TemplateTag):
     92    """
     93    The middle tag repeatable 1..n times.
     94    """
     95    grammar = Grammar('cb_one_or_more_middle middle+ end_cb_one_or_more_middle')
     96
     97@register.tag
     98class ReverseBlocks(TemplateTag):
     99    """
     100    Reverse blocks: render the last parst first and the first part last.
     101    {% cb_reverse_blocks %} ... {% next %} ... {% next %} ... {% end_cb_reverse_blocks %}
     102    """
     103    grammar = Grammar('cb_reverse_blocks next* end_cb_reverse_blocks')
     104
     105    def render(self, context):
     106        return ''.join(p.nodelist.render(context) for p in self.parse_result.parts[::-1])
     107
     108@register.tag
     109class ComplexGrammar1(TemplateTag):
     110    """
     111    Some complex grammar rule.
     112    """
     113    grammar = Grammar('cb_complex1 A* B C? D+ E? end_cb_complex1')
     114
     115@register.tag
     116class PrintParam1(TemplateTag):
     117    """
     118    Print tag parameters.
     119    {% cb_print_params "param1" "param2" ... %}
     120    """
     121    grammar = Grammar('cb_print_params')
     122
     123    def render(self, context):
     124        return ' '.join(self.parse_result.arguments)
     125
     126@register.tag
     127class PrintParam2(TemplateTag):
     128    """
     129    Print tag parameters.
     130    {% cb_print_params2 "param1" "param2" ... %} {% end_cb_print_params2 "end1" "end2" ... %}
     131    """
     132    grammar = Grammar('cb_print_params2 end_cb_print_params2')
     133
     134    def render(self, context):
     135        return ' '.join(self.parse_result.parts[0].arguments) + \
     136               self.parse_result.parts[0].nodelist.render(context) + \
     137               ' '.join(self.parse_result.parts[-1].arguments)
     138
     139
     140@register.tag
     141class PrintAndResolveParams(TemplateTag):
     142    """
     143    Print each parameters after resolving in in the context.
     144    {% cb_print_and_resolv "param1" "param2" ... %}
     145    """
     146    grammar = Grammar('cb_print_and_resolv')
     147
     148    def __init__(self, parser, parse_result):
     149        self.variables = [
     150            parser.compile_filter(a) for a in parse_result.arguments]
     151
     152    def render(self, context):
     153        return ' '.join(map(str, (v.resolve(context) for v in self.variables)))
  • tests/template_tests/test_custom.py

    diff --git a/tests/template_tests/test_custom.py b/tests/template_tests/test_custom.py
    index 4aea082..1568b8d 100644
    a b class CustomTagTests(TestCase):  
    355355            "'assignment_unlimited_args_kwargs' received multiple values for keyword argument 'eggs'",
    356356            template.Template, '{% load custom %}{% assignment_unlimited_args_kwargs 37 eggs="scrambled" eggs="scrambled" as var %}The result is: {{ var }}')
    357357
     358    def test_class_based_tags(self):
     359        c = template.Context({'value': 42})
     360
     361        t = template.Template('{% load class_based %}a{% cb_one_tag %}b')
     362        self.assertEqual(t.render(c), 'ab')
     363
     364        t = template.Template('{% load class_based %}a{% cb_two_tags %}b{% end_cb_two_tags %}c')
     365        self.assertEqual(t.render(c), 'abc')
     366
     367        t = template.Template('{% load class_based %}a{% cb_to_upper %}b{% end_cb_to_upper %}c')
     368        self.assertEqual(t.render(c), 'aBc')
     369
     370        t = template.Template('{% load class_based %}a{% cb_with_middle %}b{% middle %}c{% end_cb_with_middle %}d')
     371        self.assertEqual(t.render(c), 'abcd')
     372
     373        t = template.Template('{% load class_based %}a{% cb_render_second %}b{% middle %}c{% end_cb_render_second %}d')
     374        self.assertEqual(t.render(c), 'acd')
     375
     376        t = template.Template('{% load class_based %}a{% cb_render_second2 %}b{% middle %}c{% end_cb_render_second2 %}d')
     377        self.assertEqual(t.render(c), 'acd')
     378
     379        t = template.Template('{% load class_based %}a{% cb_optional_middle %}b{% middle %}c{% end_cb_optional_middle %}d')
     380        self.assertEqual(t.render(c), 'abcd')
     381
     382        t = template.Template('{% load class_based %}a{% cb_optional_middle %}b{% end_cb_optional_middle %}c')
     383        self.assertEqual(t.render(c), 'abc')
     384
     385        t = template.Template('{% load class_based %}a{% cb_repeating_middle %}b{% end_cb_repeating_middle %}c')
     386        self.assertEqual(t.render(c), 'abc')
     387
     388        t = template.Template('{% load class_based %}a{% cb_repeating_middle %}b{% middle %}c{% end_cb_repeating_middle %}d')
     389        self.assertEqual(t.render(c), 'abcd')
     390
     391        t = template.Template('{% load class_based %}a{% cb_repeating_middle %}b{% middle %}c{% middle %}d{% end_cb_repeating_middle %}e')
     392        self.assertEqual(t.render(c), 'abcde')
     393
     394        t = template.Template('{% load class_based %}a{% cb_repeating_middle %}b{% middle %}c{% middle %}d{% middle %}e{% end_cb_repeating_middle %}f')
     395        self.assertEqual(t.render(c), 'abcdef')
     396
     397        t = template.Template('{% load class_based %}a{% cb_one_or_more_middle %}b{% middle %}c{% end_cb_one_or_more_middle %}d')
     398        self.assertEqual(t.render(c), 'abcd')
     399
     400        t = template.Template('{% load class_based %}a{% cb_one_or_more_middle %}b{% middle %}c{% middle %}d{% end_cb_one_or_more_middle %}e')
     401        self.assertEqual(t.render(c), 'abcde')
     402
     403        t = template.Template('{% load class_based %}a{% cb_one_or_more_middle %}b{% middle %}c{% middle %}d{% middle %}e{% end_cb_one_or_more_middle %}f')
     404        self.assertEqual(t.render(c), 'abcdef')
     405
     406        t = template.Template('{% load class_based %}a{% cb_reverse_blocks %}b{% next %}c{% next %}d{% next %}e{% end_cb_reverse_blocks %}f')
     407        self.assertEqual(t.render(c), 'aedcbf')
     408
     409        t = template.Template('{% load class_based %}_{% cb_complex1 %}_{% B %}_{% C %}_{% D %}_{% end_cb_complex1 %}_')
     410        self.assertEqual(t.render(c), '______')
     411
     412        t = template.Template('{% load class_based %}_{% cb_complex1 %}_{% B %}_{% C %}_{% D %}_{% E %}_{% end_cb_complex1 %}_')
     413        self.assertEqual(t.render(c), '_______')
     414
     415        t = template.Template('{% load class_based %}_{% cb_complex1 %}_{% A %}_{% A %}_{% B %}_{% C %}_{% D %}_{% E %}_{% end_cb_complex1 %}_')
     416        self.assertEqual(t.render(c), '_________')
     417
     418        t = template.Template('{% load class_based %}_{% cb_complex1 %}_{% A %}_{% A %}_{% B %}_{% C %}_{% D %}_{% E %}_{% end_cb_complex1 %}_')
     419        self.assertEqual(t.render(c), '_________')
     420
     421        t = template.Template('{% load class_based %}_{% cb_complex1 %}_{% A %}_{% A %}_{% B %}_{% C %}_{% D %}_{% D %}_{% E %}_{% end_cb_complex1 %}_')
     422        self.assertEqual(t.render(c), '__________')
     423
     424        t = template.Template('{% load class_based %}{% cb_print_params %}')
     425        self.assertEqual(t.render(c), '')
     426
     427        t = template.Template('{% load class_based %}{% cb_print_params 5 %}')
     428        self.assertEqual(t.render(c), '5')
     429
     430        t = template.Template('{% load class_based %}{% cb_print_params value value %}')
     431        self.assertEqual(t.render(c), 'value value')
     432
     433        t = template.Template('{% load class_based %}{% cb_print_params2 start start2 %}...{% end_cb_print_params2 end end2 %}')
     434        self.assertEqual(t.render(c), 'start start2...end end2')
     435
     436        t = template.Template('{% load class_based %}{% cb_print_and_resolv value value 5 %}')
     437        self.assertEqual(t.render(c), '42 42 5')
     438
     439        # Template tag exceptions
     440        six.assertRaisesRegex(self, template.TemplateSyntaxError, 'Expected "middle" template tag, but found "end_cb_one_or_more_middle" instead.',
     441                    template.Template, '{% load class_based %}a{% cb_one_or_more_middle %}b{% end_cb_one_or_more_middle %}c')
     442
     443        # Test grammar
     444        g = template.Grammar('start middle* end')
     445        self.assertEqual(g.is_simple, False)
     446        self.assertEqual(g.first_tagname, 'start')
     447        self.assertEqual(g.as_string(), 'start middle* end')
     448
     449        g = template.Grammar('simple_tag')
     450        self.assertEqual(g.is_simple, True)
     451        self.assertEqual(g.first_tagname, 'simple_tag')
     452        self.assertEqual(g.as_string(), 'simple_tag')
     453
     454        # Grammar exceptions
     455
     456        six.assertRaisesRegex(self, template.GrammarException, 'invalid_tag# is not a valid template tag.',
     457                    template.Grammar, 'invalid_tag#')
     458
     459        six.assertRaisesRegex(self, template.GrammarException, 'The first template tag should not repeat.',
     460                    template.Grammar, 'repeat_first*')
     461
     462        six.assertRaisesRegex(self, template.GrammarException, 'The first template tag should not repeat.',
     463                    template.Grammar, 'repeat_first2+ other_tag')
     464
     465        six.assertRaisesRegex(self, template.GrammarException, 'The last template tag should not repeat.',
     466                    template.Grammar, 'repeat_last last+')
     467
     468        six.assertRaisesRegex(self, template.GrammarException, 'The template tag middle is defined more than once.',
     469                    template.Grammar, 'first middle other_middle+ again_another* middle? middle last')
     470
     471        six.assertRaisesRegex(self, template.GrammarException, 'No template tags given.',
     472                    template.Grammar, '')
     473
     474        # Registering of template tags in a library
     475        l = template.Library()
     476
     477        @l.tag
     478        class Tag(template.TemplateTag):
     479            """ Tag documentation """
     480            grammar = template.Grammar('start end')
     481
     482        self.assertEqual(l.tags['start'].grammar, Tag.grammar)
     483        self.assertEqual(l.tags['start'].__doc__, Tag.__doc__)
     484
     485        class TagWithoutGrammar(template.TemplateTag):
     486            pass
     487
     488        six.assertRaisesRegex(self, template.TemplateSyntaxError, 'TemplateTag has no valid grammar.',
     489                        l.tag, TagWithoutGrammar)
     490
     491        # Registering a tag, which is defined by a function, but without
     492        # grammar should get an UnknownGrammar instance attached
     493        l = template.Library()
     494        def tag_without_grammar(parser, token): pass
     495        l.tag(tag_without_grammar)
     496
     497        self.assertEqual(l.tags.get('tag_without_grammar', None), tag_without_grammar)
     498        self.assertEqual(isinstance(tag_without_grammar.grammar, template.UnknownGrammar), True)
     499        self.assertEqual(tag_without_grammar.grammar.first_tagname, 'tag_without_grammar')
     500
     501
    358502    def test_assignment_tag_registration(self):
    359503        # Test that the decorators preserve the decorated function's docstring, name and attributes.
    360504        self.verify_tag(custom.assignment_no_params, 'assignment_no_params')
  • new file tests/template_tests/test_grammar.py

    diff --git a/tests/template_tests/test_grammar.py b/tests/template_tests/test_grammar.py
    new file mode 100644
    index 0000000..2e9e557
    - +  
     1from __future__ import absolute_import, unicode_literals
     2
     3from django import template
     4from django.utils import six
     5from django.utils.unittest import TestCase
     6
     7class LibraryGrammarTests(TestCase):
     8    def test_library_grammar(self):
     9        # Custom tags
     10        library = template.get_library('custom')
     11        grammar = library.get_grammar()
     12        six.assertRegex(self, grammar.as_string(), 'current_app *::= current_app')
     13        six.assertRegex(self, grammar.as_string(), 'inclusion_no_params *::= inclusion_no_params')
     14
     15        # Test class based tags
     16        library = template.get_library('class_based')
     17        grammar = library.get_grammar()
     18        six.assertRegex(self, grammar.as_string(), r'cb_one_tag *::= cb_one_tag')
     19        six.assertRegex(self, grammar.as_string(), r'cb_print_params2 *::= cb_print_params2 end_cb_print_params2')
     20        six.assertRegex(self, grammar.as_string(), r'cb_complex1 *::= cb_complex1 A\* B C\? D\+ E\? end_cb_complex1')
Back to Top