Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cycler.py : 39%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Cycler
3======
5Cycling through combinations of values, producing dictionaries.
7You can add cyclers::
9 from cycler import cycler
10 cc = (cycler(color=list('rgb')) +
11 cycler(linestyle=['-', '--', '-.']))
12 for d in cc:
13 print(d)
15Results in::
17 {'color': 'r', 'linestyle': '-'}
18 {'color': 'g', 'linestyle': '--'}
19 {'color': 'b', 'linestyle': '-.'}
22You can multiply cyclers::
24 from cycler import cycler
25 cc = (cycler(color=list('rgb')) *
26 cycler(linestyle=['-', '--', '-.']))
27 for d in cc:
28 print(d)
30Results in::
32 {'color': 'r', 'linestyle': '-'}
33 {'color': 'r', 'linestyle': '--'}
34 {'color': 'r', 'linestyle': '-.'}
35 {'color': 'g', 'linestyle': '-'}
36 {'color': 'g', 'linestyle': '--'}
37 {'color': 'g', 'linestyle': '-.'}
38 {'color': 'b', 'linestyle': '-'}
39 {'color': 'b', 'linestyle': '--'}
40 {'color': 'b', 'linestyle': '-.'}
41"""
43from __future__ import (absolute_import, division, print_function,
44 unicode_literals)
46import six
47from itertools import product, cycle
48from six.moves import zip, reduce
49from operator import mul, add
50import copy
52__version__ = '0.10.0'
55def _process_keys(left, right):
56 """
57 Helper function to compose cycler keys
59 Parameters
60 ----------
61 left, right : iterable of dictionaries or None
62 The cyclers to be composed
63 Returns
64 -------
65 keys : set
66 The keys in the composition of the two cyclers
67 """
68 l_peek = next(iter(left)) if left is not None else {}
69 r_peek = next(iter(right)) if right is not None else {}
70 l_key = set(l_peek.keys())
71 r_key = set(r_peek.keys())
72 if l_key & r_key:
73 raise ValueError("Can not compose overlapping cycles")
74 return l_key | r_key
77class Cycler(object):
78 """
79 Composable cycles
81 This class has compositions methods:
83 ``+``
84 for 'inner' products (zip)
86 ``+=``
87 in-place ``+``
89 ``*``
90 for outer products (itertools.product) and integer multiplication
92 ``*=``
93 in-place ``*``
95 and supports basic slicing via ``[]``
97 Parameters
98 ----------
99 left : Cycler or None
100 The 'left' cycler
102 right : Cycler or None
103 The 'right' cycler
105 op : func or None
106 Function which composes the 'left' and 'right' cyclers.
108 """
109 def __call__(self):
110 return cycle(self)
112 def __init__(self, left, right=None, op=None):
113 """Semi-private init
115 Do not use this directly, use `cycler` function instead.
116 """
117 if isinstance(left, Cycler):
118 self._left = Cycler(left._left, left._right, left._op)
119 elif left is not None:
120 # Need to copy the dictionary or else that will be a residual
121 # mutable that could lead to strange errors
122 self._left = [copy.copy(v) for v in left]
123 else:
124 self._left = None
126 if isinstance(right, Cycler):
127 self._right = Cycler(right._left, right._right, right._op)
128 elif right is not None:
129 # Need to copy the dictionary or else that will be a residual
130 # mutable that could lead to strange errors
131 self._right = [copy.copy(v) for v in right]
132 else:
133 self._right = None
135 self._keys = _process_keys(self._left, self._right)
136 self._op = op
138 @property
139 def keys(self):
140 """
141 The keys this Cycler knows about
142 """
143 return set(self._keys)
145 def change_key(self, old, new):
146 """
147 Change a key in this cycler to a new name.
148 Modification is performed in-place.
150 Does nothing if the old key is the same as the new key.
151 Raises a ValueError if the new key is already a key.
152 Raises a KeyError if the old key isn't a key.
154 """
155 if old == new:
156 return
157 if new in self._keys:
158 raise ValueError("Can't replace %s with %s, %s is already a key" %
159 (old, new, new))
160 if old not in self._keys:
161 raise KeyError("Can't replace %s with %s, %s is not a key" %
162 (old, new, old))
164 self._keys.remove(old)
165 self._keys.add(new)
167 if self._right is not None and old in self._right.keys:
168 self._right.change_key(old, new)
170 # self._left should always be non-None
171 # if self._keys is non-empty.
172 elif isinstance(self._left, Cycler):
173 self._left.change_key(old, new)
174 else:
175 # It should be completely safe at this point to
176 # assume that the old key can be found in each
177 # iteration.
178 self._left = [{new: entry[old]} for entry in self._left]
180 def _compose(self):
181 """
182 Compose the 'left' and 'right' components of this cycle
183 with the proper operation (zip or product as of now)
184 """
185 for a, b in self._op(self._left, self._right):
186 out = dict()
187 out.update(a)
188 out.update(b)
189 yield out
191 @classmethod
192 def _from_iter(cls, label, itr):
193 """
194 Class method to create 'base' Cycler objects
195 that do not have a 'right' or 'op' and for which
196 the 'left' object is not another Cycler.
198 Parameters
199 ----------
200 label : str
201 The property key.
203 itr : iterable
204 Finite length iterable of the property values.
206 Returns
207 -------
208 cycler : Cycler
209 New 'base' `Cycler`
210 """
211 ret = cls(None)
212 ret._left = list({label: v} for v in itr)
213 ret._keys = set([label])
214 return ret
216 def __getitem__(self, key):
217 # TODO : maybe add numpy style fancy slicing
218 if isinstance(key, slice):
219 trans = self.by_key()
220 return reduce(add, (_cycler(k, v[key])
221 for k, v in six.iteritems(trans)))
222 else:
223 raise ValueError("Can only use slices with Cycler.__getitem__")
225 def __iter__(self):
226 if self._right is None:
227 return iter(dict(l) for l in self._left)
229 return self._compose()
231 def __add__(self, other):
232 """
233 Pair-wise combine two equal length cycles (zip)
235 Parameters
236 ----------
237 other : Cycler
238 The second Cycler
239 """
240 if len(self) != len(other):
241 raise ValueError("Can only add equal length cycles, "
242 "not {0} and {1}".format(len(self), len(other)))
243 return Cycler(self, other, zip)
245 def __mul__(self, other):
246 """
247 Outer product of two cycles (`itertools.product`) or integer
248 multiplication.
250 Parameters
251 ----------
252 other : Cycler or int
253 The second Cycler or integer
254 """
255 if isinstance(other, Cycler):
256 return Cycler(self, other, product)
257 elif isinstance(other, int):
258 trans = self.by_key()
259 return reduce(add, (_cycler(k, v*other)
260 for k, v in six.iteritems(trans)))
261 else:
262 return NotImplemented
264 def __rmul__(self, other):
265 return self * other
267 def __len__(self):
268 op_dict = {zip: min, product: mul}
269 if self._right is None:
270 return len(self._left)
271 l_len = len(self._left)
272 r_len = len(self._right)
273 return op_dict[self._op](l_len, r_len)
275 def __iadd__(self, other):
276 """
277 In-place pair-wise combine two equal length cycles (zip)
279 Parameters
280 ----------
281 other : Cycler
282 The second Cycler
283 """
284 if not isinstance(other, Cycler):
285 raise TypeError("Cannot += with a non-Cycler object")
286 # True shallow copy of self is fine since this is in-place
287 old_self = copy.copy(self)
288 self._keys = _process_keys(old_self, other)
289 self._left = old_self
290 self._op = zip
291 self._right = Cycler(other._left, other._right, other._op)
292 return self
294 def __imul__(self, other):
295 """
296 In-place outer product of two cycles (`itertools.product`)
298 Parameters
299 ----------
300 other : Cycler
301 The second Cycler
302 """
303 if not isinstance(other, Cycler):
304 raise TypeError("Cannot *= with a non-Cycler object")
305 # True shallow copy of self is fine since this is in-place
306 old_self = copy.copy(self)
307 self._keys = _process_keys(old_self, other)
308 self._left = old_self
309 self._op = product
310 self._right = Cycler(other._left, other._right, other._op)
311 return self
313 def __eq__(self, other):
314 """
315 Check equality
316 """
317 if len(self) != len(other):
318 return False
319 if self.keys ^ other.keys:
320 return False
322 return all(a == b for a, b in zip(self, other))
324 def __repr__(self):
325 op_map = {zip: '+', product: '*'}
326 if self._right is None:
327 lab = self.keys.pop()
328 itr = list(v[lab] for v in self)
329 return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr)
330 else:
331 op = op_map.get(self._op, '?')
332 msg = "({left!r} {op} {right!r})"
333 return msg.format(left=self._left, op=op, right=self._right)
335 def _repr_html_(self):
336 # an table showing the value of each key through a full cycle
337 output = "<table>"
338 sorted_keys = sorted(self.keys, key=repr)
339 for key in sorted_keys:
340 output += "<th>{key!r}</th>".format(key=key)
341 for d in iter(self):
342 output += "<tr>"
343 for k in sorted_keys:
344 output += "<td>{val!r}</td>".format(val=d[k])
345 output += "</tr>"
346 output += "</table>"
347 return output
349 def by_key(self):
350 """Values by key
352 This returns the transposed values of the cycler. Iterating
353 over a `Cycler` yields dicts with a single value for each key,
354 this method returns a `dict` of `list` which are the values
355 for the given key.
357 The returned value can be used to create an equivalent `Cycler`
358 using only `+`.
360 Returns
361 -------
362 transpose : dict
363 dict of lists of the values for each key.
364 """
366 # TODO : sort out if this is a bottle neck, if there is a better way
367 # and if we care.
369 keys = self.keys
370 # change this to dict comprehension when drop 2.6
371 out = dict((k, list()) for k in keys)
373 for d in self:
374 for k in keys:
375 out[k].append(d[k])
376 return out
378 # for back compatibility
379 _transpose = by_key
381 def simplify(self):
382 """Simplify the Cycler
384 Returned as a composition using only sums (no multiplications)
386 Returns
387 -------
388 simple : Cycler
389 An equivalent cycler using only summation"""
390 # TODO: sort out if it is worth the effort to make sure this is
391 # balanced. Currently it is is
392 # (((a + b) + c) + d) vs
393 # ((a + b) + (c + d))
394 # I would believe that there is some performance implications
396 trans = self.by_key()
397 return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans)))
399 def concat(self, other):
400 """Concatenate this cycler and an other.
402 The keys must match exactly.
404 This returns a single Cycler which is equivalent to
405 `itertools.chain(self, other)`
407 Examples
408 --------
410 >>> num = cycler('a', range(3))
411 >>> let = cycler('a', 'abc')
412 >>> num.concat(let)
413 cycler('a', [0, 1, 2, 'a', 'b', 'c'])
415 Parameters
416 ----------
417 other : `Cycler`
418 The `Cycler` to concatenate to this one.
420 Returns
421 -------
422 ret : `Cycler`
423 The concatenated `Cycler`
424 """
425 return concat(self, other)
428def concat(left, right):
429 """Concatenate two cyclers.
431 The keys must match exactly.
433 This returns a single Cycler which is equivalent to
434 `itertools.chain(left, right)`
436 Examples
437 --------
439 >>> num = cycler('a', range(3))
440 >>> let = cycler('a', 'abc')
441 >>> num.concat(let)
442 cycler('a', [0, 1, 2, 'a', 'b', 'c'])
444 Parameters
445 ----------
446 left, right : `Cycler`
447 The two `Cycler` instances to concatenate
449 Returns
450 -------
451 ret : `Cycler`
452 The concatenated `Cycler`
453 """
454 if left.keys != right.keys:
455 msg = '\n\t'.join(["Keys do not match:",
456 "Intersection: {both!r}",
457 "Disjoint: {just_one!r}"]).format(
458 both=left.keys & right.keys,
459 just_one=left.keys ^ right.keys)
461 raise ValueError(msg)
463 _l = left.by_key()
464 _r = right.by_key()
465 return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys))
468def cycler(*args, **kwargs):
469 """
470 Create a new `Cycler` object from a single positional argument,
471 a pair of positional arguments, or the combination of keyword arguments.
473 cycler(arg)
474 cycler(label1=itr1[, label2=iter2[, ...]])
475 cycler(label, itr)
477 Form 1 simply copies a given `Cycler` object.
479 Form 2 composes a `Cycler` as an inner product of the
480 pairs of keyword arguments. In other words, all of the
481 iterables are cycled simultaneously, as if through zip().
483 Form 3 creates a `Cycler` from a label and an iterable.
484 This is useful for when the label cannot be a keyword argument
485 (e.g., an integer or a name that has a space in it).
487 Parameters
488 ----------
489 arg : Cycler
490 Copy constructor for Cycler (does a shallow copy of iterables).
492 label : name
493 The property key. In the 2-arg form of the function,
494 the label can be any hashable object. In the keyword argument
495 form of the function, it must be a valid python identifier.
497 itr : iterable
498 Finite length iterable of the property values.
499 Can be a single-property `Cycler` that would
500 be like a key change, but as a shallow copy.
502 Returns
503 -------
504 cycler : Cycler
505 New `Cycler` for the given property
507 """
508 if args and kwargs:
509 raise TypeError("cyl() can only accept positional OR keyword "
510 "arguments -- not both.")
512 if len(args) == 1:
513 if not isinstance(args[0], Cycler):
514 raise TypeError("If only one positional argument given, it must "
515 " be a Cycler instance.")
516 return Cycler(args[0])
517 elif len(args) == 2:
518 return _cycler(*args)
519 elif len(args) > 2:
520 raise TypeError("Only a single Cycler can be accepted as the lone "
521 "positional argument. Use keyword arguments instead.")
523 if kwargs:
524 return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs)))
526 raise TypeError("Must have at least a positional OR keyword arguments")
529def _cycler(label, itr):
530 """
531 Create a new `Cycler` object from a property name and
532 iterable of values.
534 Parameters
535 ----------
536 label : hashable
537 The property key.
539 itr : iterable
540 Finite length iterable of the property values.
542 Returns
543 -------
544 cycler : Cycler
545 New `Cycler` for the given property
546 """
547 if isinstance(itr, Cycler):
548 keys = itr.keys
549 if len(keys) != 1:
550 msg = "Can not create Cycler from a multi-property Cycler"
551 raise ValueError(msg)
553 lab = keys.pop()
554 # Doesn't need to be a new list because
555 # _from_iter() will be creating that new list anyway.
556 itr = (v[lab] for v in itr)
558 return Cycler._from_iter(label, itr)