diff --git a/cycler.py b/cycler.py index cd17aa5..d6467ff 100644 --- a/cycler.py +++ b/cycler.py @@ -7,8 +7,8 @@ You can add cyclers:: from cycler import cycler - cc = (cycler('color', list('rgb')) + - cycler('linestyle', ['-', '--', '-.'])) + cc = (cycler(color=list('rgb')) + + cycler(linestyle=['-', '--', '-.'])) for d in cc: print(d) @@ -22,8 +22,8 @@ You can multiply cyclers:: from cycler import cycler - cc = (cycler('color', list('rgb')) * - cycler('linestyle', ['-', '--', '-.'])) + cc = (cycler(color=list('rgb')) * + cycler(linestyle=['-', '--', '-.'])) for d in cc: print(d) @@ -113,8 +113,8 @@ def __init__(self, left, right=None, op=None): Do not use this directly, use `cycler` function instead. """ self._keys = _process_keys(left, right) - self._left = copy.copy(left) - self._right = copy.copy(right) + self._left = copy.deepcopy(left) + self._right = copy.deepcopy(right) self._op = op @property @@ -164,7 +164,7 @@ def __getitem__(self, key): # TODO : maybe add numpy style fancy slicing if isinstance(key, slice): trans = self._transpose() - return reduce(add, (cycler(k, v[key]) + return reduce(add, (_cycler(k, v[key]) for k, v in six.iteritems(trans))) else: raise ValueError("Can only use slices with Cycler.__getitem__") @@ -203,7 +203,7 @@ def __mul__(self, other): return Cycler(self, other, product) elif isinstance(other, int): trans = self._transpose() - return reduce(add, (cycler(k, v*other) + return reduce(add, (_cycler(k, v*other) for k, v in six.iteritems(trans))) else: return NotImplemented @@ -228,11 +228,11 @@ def __iadd__(self, other): other : Cycler The second Cycler """ - old_self = copy.copy(self) + old_self = copy.deepcopy(self) self._keys = _process_keys(old_self, other) self._left = old_self self._op = zip - self._right = copy.copy(other) + self._right = copy.deepcopy(other) return self def __imul__(self, other): @@ -245,11 +245,11 @@ def __imul__(self, other): The second Cycler """ - old_self = copy.copy(self) + old_self = copy.deepcopy(self) self._keys = _process_keys(old_self, other) self._left = old_self self._op = product - self._right = copy.copy(other) + self._right = copy.deepcopy(other) return self def __eq__(self, other): @@ -329,17 +329,76 @@ def simplify(self): # I would believe that there is some performance implications trans = self._transpose() - return reduce(add, (cycler(k, v) for k, v in six.iteritems(trans))) + return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans))) -def cycler(label, itr): +def cycler(*args, **kwargs): + """ + Create a new `Cycler` object from a single positional argument, + a pair of positional arguments, or the combination of keyword arguments. + + cycler(arg) + cycler(label1=itr1[, label2=iter2[, ...]]) + cycler(label, itr) + + Form 1 simply copies a given `Cycler` object. + + Form 2 composes a `Cycler` as an inner product of the + pairs of keyword arguments. In other words, all of the + iterables are cycled simultaneously, as if through zip(). + + Form 3 creates a `Cycler` from a label and an iterable. + This is useful for when the label cannot be a keyword argument + (e.g., an integer or a name that has a space in it). + + Parameters + ---------- + arg : Cycler + Copy constructor for Cycler. + + 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. + + Returns + ------- + cycler : Cycler + New `Cycler` for the given property + + """ + if args and kwargs: + raise TypeError("cyl() can only accept positional OR keyword " + "arguments -- not both.") + + if len(args) == 1: + if not isinstance(args[0], Cycler): + raise TypeError("If only one positional argument given, it must " + " be a Cycler instance.") + return copy.deepcopy(args[0]) + elif len(args) == 2: + return _cycler(*args) + elif len(args) > 2: + raise TypeError("Only a single Cycler can be accepted as the lone " + "positional argument. Use keyword arguments instead.") + + if kwargs: + return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs))) + + raise TypeError("Must have at least a positional OR keyword arguments") + + +def _cycler(label, itr): """ Create a new `Cycler` object from a property name and iterable of values. Parameters ---------- - label : str + label : hashable The property key. itr : iterable @@ -357,7 +416,7 @@ def cycler(label, itr): raise ValueError(msg) if label in keys: - return copy.copy(itr) + return copy.deepcopy(itr) else: lab = keys.pop() itr = list(v[lab] for v in itr) diff --git a/doc/source/index.rst b/doc/source/index.rst index 5da5160..e8b3a4c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -42,7 +42,7 @@ hashable (as it will eventually be used as the key in a :obj:`dict`). from cycler import cycler - color_cycle = cycler('color', ['r', 'g', 'b']) + color_cycle = cycler(color=['r', 'g', 'b']) color_cycle The `Cycler` knows it's length and keys: @@ -61,12 +61,12 @@ the label for v in color_cycle: print(v) -`Cycler` objects can be passed as the second argument to :func:`cycler` +`Cycler` objects can be passed as the argument to :func:`cycler` which returns a new `Cycler` with a new label, but the same values. .. ipython:: python - cycler('ec', color_cycle) + cycler(ec=color_cycle) Iterating over a `Cycler` results in the finite list of entries, to @@ -94,12 +94,12 @@ Equal length `Cycler` s with different keys can be added to get the .. ipython:: python - lw_cycle = cycler('lw', range(1, 4)) + lw_cycle = cycler(lw=range(1, 4)) wc = lw_cycle + color_cycle The result has the same length and has keys which are the union of the -two input `Cycler` s. +two input `Cycler`'s. .. ipython:: python @@ -123,6 +123,17 @@ As with arithmetic, addition is commutative for j, (a, b) in enumerate(zip(lw_c, c_lw)): print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b)) +For convenience, the :func:`cycler` function can have multiple +key-value pairs and will automatically compose them into a single +`Cycler` via addition + +.. ipython:: python + + wc = cycler(c=['r', 'g', 'b'], lw=range(3)) + + for s in wc: + print(s) + Multiplication ~~~~~~~~~~~~~~ @@ -131,7 +142,7 @@ Any pair of `Cycler` can be multiplied .. ipython:: python - m_cycle = cycler('marker', ['s', 'o']) + m_cycle = cycler(marker=['s', 'o']) m_c = m_cycle * color_cycle @@ -199,7 +210,7 @@ We can use `Cycler` instances to cycle over one or more ``kwarg`` to figsize=(8, 4)) x = np.arange(10) - color_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler(c=['r', 'g', 'b']) for i, sty in enumerate(color_cycle): ax1.plot(x, x*(i+1), **sty) @@ -219,7 +230,7 @@ We can use `Cycler` instances to cycle over one or more ``kwarg`` to figsize=(8, 4)) x = np.arange(10) - color_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler(c=['r', 'g', 'b']) ls_cycle = cycler('ls', ['-', '--']) lw_cycle = cycler('lw', range(1, 4)) @@ -243,14 +254,14 @@ A :obj:`ValueError` is raised if unequal length `Cycler` s are added together .. ipython:: python :okexcept: - cycler('c', ['r', 'g', 'b']) + cycler('ls', ['-', '--']) + cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--']) or if two cycles which have overlapping keys are composed .. ipython:: python :okexcept: - color_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler(c=['r', 'g', 'b']) color_cycle + color_cycle diff --git a/test_cycler.py b/test_cycler.py index a48b6df..3a1b426 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -24,16 +24,18 @@ def _cycles_equal(c1, c2): def test_creation(): - c = cycler('c', 'rgb') + c = cycler(c='rgb') yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] - c = cycler('c', list('rgb')) + c = cycler(c=list('rgb')) + yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] + c = cycler(cycler(c='rgb')) yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] def test_compose(): - c1 = cycler('c', 'rgb') - c2 = cycler('lw', range(3)) - c3 = cycler('lw', range(15)) + c1 = cycler(c='rgb') + c2 = cycler(lw=range(3)) + c3 = cycler(lw=range(15)) # addition yield _cycler_helper, c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)] yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] @@ -54,75 +56,87 @@ def test_compose(): def test_inplace(): - c1 = cycler('c', 'rgb') - c2 = cycler('lw', range(3)) + c1 = cycler(c='rgb') + c2 = cycler(lw=range(3)) c2 += c1 yield _cycler_helper, c2, 3, ['c', 'lw'], [list('rgb'), range(3)] - c3 = cycler('c', 'rgb') - c4 = cycler('lw', range(3)) + c3 = cycler(c='rgb') + c4 = cycler(lw=range(3)) c3 *= c4 target = zip(*product(list('rgb'), range(3))) yield (_cycler_helper, c3, 9, ['c', 'lw'], target) def test_constructor(): - c1 = cycler('c', 'rgb') - c2 = cycler('ec', c1) + c1 = cycler(c='rgb') + c2 = cycler(ec=c1) yield _cycler_helper, c1+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 - c3 = cycler('c', c1) + c3 = cycler(c=c1) yield _cycler_helper, c3+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 + # Using a non-string hashable + c4 = cycler(1, range(3)) + yield _cycler_helper, c4+c1, 3, [1, 'c'], [range(3), ['r', 'g', 'b']] + + # addition using cycler() + yield (_cycler_helper, cycler(c='rgb', lw=range(3)), + 3, ['c', 'lw'], [list('rgb'), range(3)]) + yield (_cycler_helper, cycler(lw=range(3), c='rgb'), + 3, ['c', 'lw'], [list('rgb'), range(3)]) + # Purposely mixing them + yield (_cycler_helper, cycler(c=range(3), lw=c1), + 3, ['c', 'lw'], [range(3), list('rgb')]) def test_failures(): - c1 = cycler('c', 'rgb') - c2 = cycler('c', c1) + c1 = cycler(c='rgb') + c2 = cycler(c=c1) assert_raises(ValueError, add, c1, c2) assert_raises(ValueError, iadd, c1, c2) assert_raises(ValueError, mul, c1, c2) assert_raises(ValueError, imul, c1, c2) - c3 = cycler('ec', c1) + c3 = cycler(ec=c1) - assert_raises(ValueError, cycler, 'c', c2 + c3) + assert_raises(ValueError, cycler, c=c2+c3) def test_simplify(): - c1 = cycler('c', 'rgb') - c2 = cycler('ec', c1) + c1 = cycler(c='rgb') + c2 = cycler(ec=c1) for c in [c1 * c2, c2 * c1, c1 + c2]: yield _cycles_equal, c, c.simplify() def test_multiply(): - c1 = cycler('c', 'rgb') + c1 = cycler(c='rgb') yield _cycler_helper, 2*c1, 6, ['c'], ['rgb'*2] - c2 = cycler('ec', c1) + c2 = cycler(ec=c1) c3 = c1 * c2 yield _cycles_equal, 2*c3, c3*2 def test_mul_fails(): - c1 = cycler('c', 'rgb') + c1 = cycler(c='rgb') assert_raises(TypeError, mul, c1, 2.0) assert_raises(TypeError, mul, c1, 'a') assert_raises(TypeError, mul, c1, []) def test_getitem(): - c1 = cycler('lw', range(15)) + c1 = cycler(3, range(15)) widths = list(range(15)) for slc in (slice(None, None, None), slice(None, None, -1), slice(1, 5, None), slice(0, 5, 2)): - yield _cycles_equal, c1[slc], cycler('lw', widths[slc]) + yield _cycles_equal, c1[slc], cycler(3, widths[slc]) def test_fail_getime(): - c1 = cycler('lw', range(15)) + c1 = cycler(lw=range(15)) assert_raises(ValueError, Cycler.__getitem__, c1, 0) assert_raises(ValueError, Cycler.__getitem__, c1, [0, 1]) @@ -135,24 +149,25 @@ def _repr_tester_helper(rpr_func, cyc, target_repr): def test_repr(): - c = cycler('c', 'rgb') - c2 = cycler('lw', range(3)) + c = cycler(c='rgb') + # Using an identifier that would be not valid as a kwarg + c2 = cycler('3rd', range(3)) - c_sum_rpr = "(cycler('c', ['r', 'g', 'b']) + cycler('lw', [0, 1, 2]))" - c_prod_rpr = "(cycler('c', ['r', 'g', 'b']) * cycler('lw', [0, 1, 2]))" + c_sum_rpr = "(cycler('c', ['r', 'g', 'b']) + cycler('3rd', [0, 1, 2]))" + c_prod_rpr = "(cycler('c', ['r', 'g', 'b']) * cycler('3rd', [0, 1, 2]))" yield _repr_tester_helper, '__repr__', c + c2, c_sum_rpr yield _repr_tester_helper, '__repr__', c * c2, c_prod_rpr - sum_html = "
'c''lw'
'r'0
'g'1
'b'2
" - prod_html = "
'c''lw'
'r'0
'r'1
'r'2
'g'0
'g'1
'g'2
'b'0
'b'1
'b'2
" + sum_html = "
'3rd''c'
0'r'
1'g'
2'b'
" + prod_html = "
'3rd''c'
0'r'
1'r'
2'r'
0'g'
1'g'
2'g'
0'b'
1'b'
2'b'
" yield _repr_tester_helper, '_repr_html_', c + c2, sum_html yield _repr_tester_helper, '_repr_html_', c * c2, prod_html def test_call(): - c = cycler('c', 'rgb') + c = cycler(c='rgb') c_cycle = c() assert_true(isinstance(c_cycle, cycle)) j = 0 @@ -171,14 +186,14 @@ def _eq_test_helper(a, b, res): def test_eq(): - a = cycler('c', 'rgb') - b = cycler('c', 'rgb') + a = cycler(c='rgb') + b = cycler(c='rgb') yield _eq_test_helper, a, b, True yield _eq_test_helper, a, b[::-1], False - c = cycler('lw', range(3)) + c = cycler(lw=range(3)) yield _eq_test_helper, a+c, c+a, True yield _eq_test_helper, a+c, c+b, True yield _eq_test_helper, a*c, c*a, False yield _eq_test_helper, a, c, False - d = cycler('c', 'ymk') + d = cycler(c='ymk') yield _eq_test_helper, b, d, False