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