diff --git a/cycler.py b/cycler.py index 1cd4332..f31fe85 100644 --- a/cycler.py +++ b/cycler.py @@ -57,16 +57,17 @@ def _process_keys(left, right): """ - Helper function to compose cycler keys + Helper function to compose cycler keys. Parameters ---------- left, right : iterable of dictionaries or None - The cyclers to be composed + The cyclers to be composed. + Returns ------- keys : set - The keys in the composition of the two cyclers + The keys in the composition of the two cyclers. """ l_peek = next(iter(left)) if left is not None else {} r_peek = next(iter(right)) if right is not None else {} @@ -77,9 +78,38 @@ def _process_keys(left, right): return l_key | r_key +def concat(left, right): + r""" + Concatenate `Cycler`\s, as if chained using `itertools.chain`. + + The keys must match exactly. + + Examples + -------- + >>> num = cycler('a', range(3)) + >>> let = cycler('a', 'abc') + >>> num.concat(let) + cycler('a', [0, 1, 2, 'a', 'b', 'c']) + + Returns + ------- + `Cycler` + The concatenated cycler. + """ + if left.keys != right.keys: + raise ValueError("Keys do not match:\n" + "\tIntersection: {both!r}\n" + "\tDisjoint: {just_one!r}".format( + both=left.keys & right.keys, + just_one=left.keys ^ right.keys)) + _l = left.by_key() + _r = right.by_key() + return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) + + class Cycler(object): """ - Composable cycles + Composable cycles. This class has compositions methods: @@ -95,25 +125,22 @@ class Cycler(object): ``*=`` in-place ``*`` - and supports basic slicing via ``[]`` + and supports basic slicing via ``[]``. Parameters ---------- - left : Cycler or None - The 'left' cycler - - right : Cycler or None - The 'right' cycler - + left, right : Cycler or None + The 'left' and 'right' cyclers. op : func or None Function which composes the 'left' and 'right' cyclers. - """ + def __call__(self): return cycle(self) def __init__(self, left, right=None, op=None): - """Semi-private init + """ + Semi-private init. Do not use this directly, use `cycler` function instead. """ @@ -143,9 +170,7 @@ def __contains__(self, k): @property def keys(self): - """ - The keys this Cycler knows about - """ + """The keys this Cycler knows about.""" return set(self._keys) def change_key(self, old, new): @@ -156,7 +181,6 @@ def change_key(self, old, new): Does nothing if the old key is the same as the new key. Raises a ValueError if the new key is already a key. Raises a KeyError if the old key isn't a key. - """ if old == new: return @@ -183,16 +207,6 @@ def change_key(self, old, new): # iteration. self._left = [{new: entry[old]} for entry in self._left] - def _compose(self): - """ - Compose the 'left' and 'right' components of this cycle - """ - for a, b in self._op(self._left, self._right): - out = dict() - out.update(a) - out.update(b) - yield out - @classmethod def _from_iter(cls, label, itr): """ @@ -210,8 +224,8 @@ def _from_iter(cls, label, itr): Returns ------- - cycler : Cycler - New 'base' `Cycler` + `Cycler` + New 'base' cycler. """ ret = cls(None) ret._left = list({label: v} for v in itr) @@ -228,18 +242,22 @@ def __getitem__(self, key): def __iter__(self): if self._right is None: - return iter(dict(l) for l in self._left) - - return self._compose() + for l in self._left: + yield dict(l) + else: + for a, b in self._op(self._left, self._right): + out = dict() + out.update(a) + out.update(b) + yield out def __add__(self, other): """ - Pair-wise combine two equal length cycles (zip) + Pair-wise combine two equal length cyclers (zip). Parameters ---------- other : Cycler - The second Cycler """ if len(self) != len(other): raise ValueError("Can only add equal length cycles, " @@ -248,13 +266,12 @@ def __add__(self, other): def __mul__(self, other): """ - Outer product of two cycles (`itertools.product`) or integer + Outer product of two cyclers (`itertools.product`) or integer multiplication. Parameters ---------- other : Cycler or int - The second Cycler or integer """ if isinstance(other, Cycler): return Cycler(self, other, product) @@ -277,12 +294,11 @@ def __len__(self): def __iadd__(self, other): """ - In-place pair-wise combine two equal length cycles (zip) + In-place pair-wise combine two equal length cyclers (zip). Parameters ---------- other : Cycler - The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot += with a non-Cycler object") @@ -296,12 +312,11 @@ def __iadd__(self, other): def __imul__(self, other): """ - In-place outer product of two cycles (`itertools.product`) + In-place outer product of two cyclers (`itertools.product`). Parameters ---------- other : Cycler - The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot *= with a non-Cycler object") @@ -314,14 +329,10 @@ def __imul__(self, other): return self def __eq__(self, other): - """ - Check equality - """ if len(self) != len(other): return False if self.keys ^ other.keys: return False - return all(a == b for a, b in zip(self, other)) def __ne__(self, other): @@ -355,7 +366,8 @@ def _repr_html_(self): return output def by_key(self): - """Values by key + """ + Values by key. This returns the transposed values of the cycler. Iterating over a `Cycler` yields dicts with a single value for each key, @@ -386,90 +398,22 @@ def by_key(self): _transpose = by_key def simplify(self): - """Simplify the Cycler - - Returned as a composition using only sums (no multiplications) + """ + Simplify the cycler into a sum (but no products) of cyclers. Returns ------- simple : Cycler - An equivalent cycler using only summation""" + """ # TODO: sort out if it is worth the effort to make sure this is # balanced. Currently it is is # (((a + b) + c) + d) vs # ((a + b) + (c + d)) # I would believe that there is some performance implications - trans = self.by_key() return reduce(add, (_cycler(k, v) for k, v in trans.items())) - def concat(self, other): - """Concatenate this cycler and an other. - - The keys must match exactly. - - This returns a single Cycler which is equivalent to - `itertools.chain(self, other)` - - Examples - -------- - - >>> num = cycler('a', range(3)) - >>> let = cycler('a', 'abc') - >>> num.concat(let) - cycler('a', [0, 1, 2, 'a', 'b', 'c']) - - Parameters - ---------- - other : `Cycler` - The `Cycler` to concatenate to this one. - - Returns - ------- - ret : `Cycler` - The concatenated `Cycler` - """ - return concat(self, other) - - -def concat(left, right): - """Concatenate two cyclers. - - The keys must match exactly. - - This returns a single Cycler which is equivalent to - `itertools.chain(left, right)` - - Examples - -------- - - >>> num = cycler('a', range(3)) - >>> let = cycler('a', 'abc') - >>> num.concat(let) - cycler('a', [0, 1, 2, 'a', 'b', 'c']) - - Parameters - ---------- - left, right : `Cycler` - The two `Cycler` instances to concatenate - - Returns - ------- - ret : `Cycler` - The concatenated `Cycler` - """ - if left.keys != right.keys: - msg = '\n\t'.join(["Keys do not match:", - "Intersection: {both!r}", - "Disjoint: {just_one!r}"]).format( - both=left.keys & right.keys, - just_one=left.keys ^ right.keys) - - raise ValueError(msg) - - _l = left.by_key() - _r = right.by_key() - return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) + concat = concat def cycler(*args, **kwargs): @@ -495,12 +439,10 @@ def cycler(*args, **kwargs): ---------- arg : Cycler Copy constructor for Cycler (does a shallow copy of iterables). - label : name The property key. In the 2-arg form of the function, the label can be any hashable object. In the keyword argument form of the function, it must be a valid python identifier. - itr : iterable Finite length iterable of the property values. Can be a single-property `Cycler` that would @@ -535,14 +477,12 @@ def cycler(*args, **kwargs): def _cycler(label, itr): """ - Create a new `Cycler` object from a property name and - iterable of values. + Create a new `Cycler` object from a property name and iterable of values. Parameters ---------- label : hashable The property key. - itr : iterable Finite length iterable of the property values.