1"""
2Helper for looping over sequences, particular in templates.
3
4Often in a loop in a template it's handy to know what's next up,
5previously up, if this is the first or last item in the sequence, etc.
6These can be awkward to manage in a normal Python loop, but using the
7looper you can get a better sense of the context.  Use like::
8
9    >>> for loop, item in looper(['a', 'b', 'c']):
10    ...     print loop.number, item
11    ...     if not loop.last:
12    ...         print '---'
13    1 a
14    ---
15    2 b
16    ---
17    3 c
18
19"""
20import sys
21from .compat3 import basestring_
22
23__all__ = ['looper']
24
25
26class looper:
27    """
28    Helper for looping (particularly in templates)
29
30    Use this like::
31
32        for loop, item in looper(seq):
33            if loop.first:
34                ...
35    """
36
37    def __init__(self, seq):
38        self.seq = seq
39
40    def __iter__(self):
41        return looper_iter(self.seq)
42
43    def __repr__(self):
44        return '<%s for %r>' % (
45            self.__class__.__name__, self.seq)
46
47
48class looper_iter:
49
50    def __init__(self, seq):
51        self.seq = list(seq)
52        self.pos = 0
53
54    def __iter__(self):
55        return self
56
57    def __next__(self):
58        if self.pos >= len(self.seq):
59            raise StopIteration
60        result = loop_pos(self.seq, self.pos), self.seq[self.pos]
61        self.pos += 1
62        return result
63
64
65class loop_pos:
66
67    def __init__(self, seq, pos):
68        self.seq = seq
69        self.pos = pos
70
71    def __repr__(self):
72        return '<loop pos=%r at %r>' % (
73            self.seq[self.pos], self.pos)
74
75    @property
76    def index(self):
77        return self.pos
78
79    @property
80    def number(self):
81        return self.pos + 1
82
83    @property
84    def item(self):
85        return self.seq[self.pos]
86
87    @property
88    def __next__(self):
89        try:
90            return self.seq[self.pos + 1]
91        except IndexError:
92            return None
93
94    @property
95    def previous(self):
96        if self.pos == 0:
97            return None
98        return self.seq[self.pos - 1]
99
100    @property
101    def odd(self):
102        return not self.pos % 2
103
104    @property
105    def even(self):
106        return self.pos % 2
107
108    @property
109    def first(self):
110        return self.pos == 0
111
112    @property
113    def last(self):
114        return self.pos == len(self.seq) - 1
115
116    @property
117    def length(self):
118        return len(self.seq)
119
120    def first_group(self, getter=None):
121        """
122        Returns true if this item is the start of a new group,
123        where groups mean that some attribute has changed.  The getter
124        can be None (the item itself changes), an attribute name like
125        ``'.attr'``, a function, or a dict key or list index.
126        """
127        if self.first:
128            return True
129        return self._compare_group(self.item, self.previous, getter)
130
131    def last_group(self, getter=None):
132        """
133        Returns true if this item is the end of a new group,
134        where groups mean that some attribute has changed.  The getter
135        can be None (the item itself changes), an attribute name like
136        ``'.attr'``, a function, or a dict key or list index.
137        """
138        if self.last:
139            return True
140        return self._compare_group(self.item, self.__next__, getter)
141
142    def _compare_group(self, item, other, getter):
143        if getter is None:
144            return item != other
145        elif (isinstance(getter, basestring_) and getter.startswith('.')):
146            getter = getter[1:]
147            if getter.endswith('()'):
148                getter = getter[:-2]
149                return getattr(item, getter)() != getattr(other, getter)()
150            else:
151                return getattr(item, getter) != getattr(other, getter)
152        elif hasattr(getter, '__call__'):
153            return getter(item) != getter(other)
154        else:
155            return item[getter] != other[getter]
156