Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 62 additions & 122 deletions cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -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:

Expand All @@ -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.
"""
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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):
"""
Expand All @@ -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)
Expand All @@ -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, "
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down