Ticket #3523: unpacking-for.diff

File unpacking-for.diff, 9.0 KB (added by russellm, 8 years ago)

Slightly improved for loop unpacking implementation

  • django/template/defaulttags.py

     
    55from django.template import get_library, Library, InvalidTemplateLibrary
    66from django.conf import settings
    77import sys
     8import re
    89
    910register = Library()
    1011
     
    6162        return ''
    6263
    6364class ForNode(Node):
    64     def __init__(self, loopvar, sequence, reversed, nodelist_loop):
    65         self.loopvar, self.sequence = loopvar, sequence
     65    def __init__(self, loopvars, sequence, reversed, nodelist_loop):
     66        self.loopvars, self.sequence = loopvars, sequence
    6667        self.reversed = reversed
    6768        self.nodelist_loop = nodelist_loop
    6869
     
    7273        else:
    7374            reversed = ''
    7475        return "<For Node: for %s in %s, tail_len: %d%s>" % \
    75             (self.loopvar, self.sequence, len(self.nodelist_loop), reversed)
     76            (', '.join( self.loopvars ), self.sequence, len(self.nodelist_loop), reversed)
    7677
    7778    def __iter__(self):
    7879        for node in self.nodelist_loop:
     
    107108                for index in range(len(data)-1, -1, -1):
    108109                    yield data[index]
    109110            values = reverse(values)
     111        unpack = len(self.loopvars) > 1
    110112        for i, item in enumerate(values):
    111113            context['forloop'] = {
    112114                # shortcuts for current loop iteration number
     
    120122                'last': (i == len_values - 1),
    121123                'parentloop': parentloop,
    122124            }
    123             context[self.loopvar] = item
     125            if unpack:
     126                # If there are multiple loop variables, unpack the item into them.
     127                context.update(dict(zip(self.loopvars, item)))
     128            else:
     129                context[self.loopvars[0]] = item
    124130            for node in self.nodelist_loop:
    125131                nodelist.append(node.render(context))
     132            if unpack:
     133                # The loop variables were pushed on to the context so pop them
     134                # off again. This is necessary because the tag lets the length
     135                # of loopvars differ to the length of each set of items and we
     136                # don't want to leave any vars from the previous loop on the
     137                # context.
     138                context.pop()
    126139        context.pop()
    127140        return nodelist.render(context)
    128141
     
    486499    nodelist = parser.parse(('endfilter',))
    487500    parser.delete_first_token()
    488501    return FilterNode(filter_expr, nodelist)
    489 filter = register.tag("filter", do_filter)
     502do_filter = register.tag("filter", do_filter)
    490503
    491504#@register.tag
    492505def firstof(parser, token):
     
    530543        {% endfor %}
    531544        </ul>
    532545
    533     You can also loop over a list in reverse by using
     546    You can loop over a list in reverse by using
    534547    ``{% for obj in list reversed %}``.
     548   
     549    You can also unpack multiple values from a two-dimensional array::
     550   
     551        {% for key,value in dict.items %}
     552            {{ key }}: {{ value }}
     553        {% endfor %}
    535554
    536555    The for loop sets a number of variables available within the loop:
    537556
     
    552571
    553572    """
    554573    bits = token.contents.split()
    555     if len(bits) == 5 and bits[4] != 'reversed':
    556         raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents
    557     if len(bits) not in (4, 5):
    558         raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents
    559     if bits[2] != 'in':
    560         raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents
    561     loopvar = bits[1]
    562     sequence = parser.compile_filter(bits[3])
    563     reversed = (len(bits) == 5)
     574    if len(bits) < 4:
     575        raise TemplateSyntaxError, "'for' statements should have at least four words: %s" % token.contents
     576
     577    reversed = bits[-1] == 'reversed'
     578    in_index = reversed and -3 or -2
     579    if bits[in_index] != 'in':
     580        raise TemplateSyntaxError, "'for' statements should use the format 'for x in y': %s" % token.contents
     581
     582    loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',')
     583    for var in loopvars:
     584        if not var or ' ' in var:
     585            raise TemplateSyntaxError, "'for' tag received an invalid argument: %s" % token.contents
     586
     587    sequence = parser.compile_filter(bits[in_index+1])
    564588    nodelist_loop = parser.parse(('endfor',))
    565589    parser.delete_first_token()
    566     return ForNode(loopvar, sequence, reversed, nodelist_loop)
     590    return ForNode(loopvars, sequence, reversed, nodelist_loop)
    567591do_for = register.tag("for", do_for)
    568592
    569593def do_ifequal(parser, token, negate):
  • tests/regressiontests/templates/tests.py

     
    289289            'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"),
    290290            'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"),
    291291            'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"),
     292            'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
     293            'for-tag-unpack03': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
     294            'for-tag-unpack04': ("{% for key , value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
     295            'for-tag-unpack05': ("{% for key ,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
     296            'for-tag-unpack06': ("{% for key value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
     297            'for-tag-unpack07': ("{% for key,,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
     298            'for-tag-unpack08': ("{% for key,value, in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
     299            # Ensure that a single loopvar doesn't truncate the list in val.
     300            'for-tag-unpack09': ("{% for val in items %}{{ val.0 }}:{{ val.1 }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
     301            # Otherwise, silently truncate if the length of loopvars differs to the length of each set of items.
     302            'for-tag-unpack10': ("{% for x,y in items %}{{ x }}:{{ y }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'orange'))}, "one:1/two:2/"),
     303            'for-tag-unpack11': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, ("one:1,/two:2,/", "one:1,INVALID/two:2,INVALID/")),
     304            'for-tag-unpack12': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2))}, ("one:1,carrot/two:2,/", "one:1,carrot/two:2,INVALID/")),
     305            'for-tag-unpack13': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'cheese'))}, ("one:1,carrot/two:2,cheese/", "one:1,carrot/two:2,cheese/")),
    292306
    293307            ### IF TAG ################################################################
    294308            'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),
  • docs/templates.txt

     
    444444~~~
    445445
    446446Loop over each item in an array.  For example, to display a list of athletes
    447 given ``athlete_list``::
     447provided in ``athlete_list``::
    448448
    449449    <ul>
    450450    {% for athlete in athlete_list %}
     
    452452    {% endfor %}
    453453    </ul>
    454454
    455 You can also loop over a list in reverse by using ``{% for obj in list reversed %}``.
     455You can loop over a list in reverse by using ``{% for obj in list reversed %}``.
    456456
     457If you need to loop over a list of lists, you can unpack the values
     458in each sub-list into a set of known names. For example, if your context contains
     459a list of (x,y) coordinates called ``points``, you could use the following
     460to output the list of points::
     461
     462    {% for x, y in points %}
     463        There is a point at {{ x }},{{ y }}
     464    {% endfor %}
     465   
     466This can also be useful if you need to access the values in a dictionary.
     467For example, if your context contained a dictionary ``dict``, the following
     468would display the keys and values of the dictionary::
     469
     470    {% for key, value in dict.items %}
     471        {{ key }}: {{ value }}
     472    {% endfor %}
     473
    457474The for loop sets a number of variables available within the loop:
    458475
    459476    ==========================  ================================================
Back to Top