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