Skip to content
Merged
Show file tree
Hide file tree
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
43 changes: 41 additions & 2 deletions cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,18 @@ def __init__(self, left, right=None, op=None):
if isinstance(left, Cycler):
self._left = Cycler(left._left, left._right, left._op)
elif left is not None:
self._left = list(left)
# Need to copy the dictionary or else that will be a residual
# mutable that could lead to strange errors
self._left = [copy.copy(v) for v in left]
else:
self._left = None

if isinstance(right, Cycler):
self._right = Cycler(right._left, right._right, right._op)
elif right is not None:
self._right = list(right)
# Need to copy the dictionary or else that will be a residual
# mutable that could lead to strange errors
self._right = [copy.copy(v) for v in right]
else:
self._right = None

Expand All @@ -138,6 +142,41 @@ def keys(self):
"""
return set(self._keys)

def change_key(self, old, new):
"""
Change a key in this cycler to a new name.
Modification is performed in-place.

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
if new in self._keys:
raise ValueError("Can't replace %s with %s, %s is already a key" %
(old, new, new))
if old not in self._keys:
raise KeyError("Can't replace %s with %s, %s is not a key" %
(old, new, old))

self._keys.remove(old)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be done in the base case of the recursion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

erm, maybe. Apparently every level holds it's keys instead of consulting their children...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that actually made sense to me. This is something that doesn't need to be recomputed on the fly all the time, and since the cyclers are all deep-copied, I am not concerned about someone changing a key in an underlying cycler (although, I should add a test for that...).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, looks like I didn't rebase properly! Once I rebased on top of the recently merged PR which deepcopies just about everything, the above problem goes away. Now, I will see about the original failure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and the original problem is solved, and the remaining exception turns out to be an error in the test I wrote. Now, I'll add a some more tests to make sure what we just encountered doesn't happen again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it is a good thing I did that. Found a way to break cycler.

self._keys.add(new)

if self._right is not None and old in self._right.keys:
self._right.change_key(old, new)

# self._left should always be non-None
# if self._keys is non-empty.
elif isinstance(self._left, Cycler):
self._left.change_key(old, new)
else:
# It should be completely safe at this point to
# assume that the old key can be found in each
# iteration.
self._left = [{new: entry[old]} for entry in self._left]

def _compose(self):
"""
Compose the 'left' and 'right' components of this cycle
Expand Down
30 changes: 30 additions & 0 deletions test_cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,36 @@ def test_copying():
assert_equal(c3, cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))


def test_keychange():
c1 = cycler('c', 'rgb')
c2 = cycler('lw', [1, 2, 3])
c3 = cycler('ec', 'yk')

c3.change_key('ec', 'edgecolor')
assert_equal(c3, cycler('edgecolor', c3))

c = c1 + c2
c.change_key('lw', 'linewidth')
# Changing a key in one cycler should have no
# impact in the original cycler.
assert_equal(c2, cycler('lw', [1, 2, 3]))
assert_equal(c, c1 + cycler('linewidth', c2))

c = (c1 + c2) * c3
c.change_key('c', 'color')
assert_equal(c1, cycler('c', 'rgb'))
assert_equal(c, (cycler('color', c1) + c2) * c3)

# Perfectly fine, it is a no-op
c.change_key('color', 'color')
assert_equal(c, (cycler('color', c1) + c2) * c3)

# Can't change a key to one that is already in there
assert_raises(ValueError, Cycler.change_key, c, 'color', 'lw')
# Can't change a key you don't have
assert_raises(KeyError, Cycler.change_key, c, 'c', 'foobar')


def _eq_test_helper(a, b, res):
if res:
assert_equal(a, b)
Expand Down