Skip to content

Commit 68b4995

Browse files
committed
Merge pull request #15 from WeatherGod/copy_fixes
FIX: some copying issues.
2 parents cd0e03c + 786cc49 commit 68b4995

File tree

2 files changed

+76
-18
lines changed

2 files changed

+76
-18
lines changed

cycler.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ def _process_keys(left, right):
5858
5959
Parameters
6060
----------
61-
left, right : Cycler or None
61+
left, right : iterable of dictionaries or None
6262
The cyclers to be composed
6363
Returns
6464
-------
6565
keys : set
6666
The keys in the composition of the two cyclers
6767
"""
68-
l_key = left.keys if left is not None else set()
69-
r_key = right.keys if right is not None else set()
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())
7072
if l_key & r_key:
7173
raise ValueError("Can not compose overlapping cycles")
7274
return l_key | r_key
@@ -112,9 +114,21 @@ def __init__(self, left, right=None, op=None):
112114
113115
Do not use this directly, use `cycler` function instead.
114116
"""
115-
self._keys = _process_keys(left, right)
116-
self._left = copy.deepcopy(left)
117-
self._right = copy.deepcopy(right)
117+
if isinstance(left, Cycler):
118+
self._left = Cycler(left._left, left._right, left._op)
119+
elif left is not None:
120+
self._left = list(left)
121+
else:
122+
self._left = None
123+
124+
if isinstance(right, Cycler):
125+
self._right = Cycler(right._left, right._right, right._op)
126+
elif right is not None:
127+
self._right = list(right)
128+
else:
129+
self._right = None
130+
131+
self._keys = _process_keys(self._left, self._right)
118132
self._op = op
119133

120134
@property
@@ -228,11 +242,14 @@ def __iadd__(self, other):
228242
other : Cycler
229243
The second Cycler
230244
"""
231-
old_self = copy.deepcopy(self)
245+
if not isinstance(other, Cycler):
246+
raise TypeError("Cannot += with a non-Cycler object")
247+
# True shallow copy of self is fine since this is in-place
248+
old_self = copy.copy(self)
232249
self._keys = _process_keys(old_self, other)
233250
self._left = old_self
234251
self._op = zip
235-
self._right = copy.deepcopy(other)
252+
self._right = Cycler(other._left, other._right, other._op)
236253
return self
237254

238255
def __imul__(self, other):
@@ -244,12 +261,14 @@ def __imul__(self, other):
244261
other : Cycler
245262
The second Cycler
246263
"""
247-
248-
old_self = copy.deepcopy(self)
264+
if not isinstance(other, Cycler):
265+
raise TypeError("Cannot *= with a non-Cycler object")
266+
# True shallow copy of self is fine since this is in-place
267+
old_self = copy.copy(self)
249268
self._keys = _process_keys(old_self, other)
250269
self._left = old_self
251270
self._op = product
252-
self._right = copy.deepcopy(other)
271+
self._right = Cycler(other._left, other._right, other._op)
253272
return self
254273

255274
def __eq__(self, other):
@@ -354,7 +373,7 @@ def cycler(*args, **kwargs):
354373
Parameters
355374
----------
356375
arg : Cycler
357-
Copy constructor for Cycler.
376+
Copy constructor for Cycler (does a shallow copy of iterables).
358377
359378
label : name
360379
The property key. In the 2-arg form of the function,
@@ -363,6 +382,8 @@ def cycler(*args, **kwargs):
363382
364383
itr : iterable
365384
Finite length iterable of the property values.
385+
Can be a single-property `Cycler` that would
386+
be like a key change, but as a shallow copy.
366387
367388
Returns
368389
-------
@@ -378,7 +399,7 @@ def cycler(*args, **kwargs):
378399
if not isinstance(args[0], Cycler):
379400
raise TypeError("If only one positional argument given, it must "
380401
" be a Cycler instance.")
381-
return copy.deepcopy(args[0])
402+
return Cycler(args[0])
382403
elif len(args) == 2:
383404
return _cycler(*args)
384405
elif len(args) > 2:
@@ -415,10 +436,9 @@ def _cycler(label, itr):
415436
msg = "Can not create Cycler from a multi-property Cycler"
416437
raise ValueError(msg)
417438

418-
if label in keys:
419-
return copy.deepcopy(itr)
420-
else:
421-
lab = keys.pop()
422-
itr = list(v[lab] for v in itr)
439+
lab = keys.pop()
440+
# Doesn't need to be a new list because
441+
# _from_iter() will be creating that new list anyway.
442+
itr = (v[lab] for v in itr)
423443

424444
return Cycler._from_iter(label, itr)

test_cycler.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,44 @@ def test_call():
178178
assert_equal(j, len(c) * 2)
179179

180180

181+
def test_copying():
182+
# Just about everything results in copying the cycler and
183+
# its contents (shallow). This set of tests is intended to make sure
184+
# of that. Our iterables will be mutable for extra fun!
185+
i1 = [1, 2, 3]
186+
i2 = ['r', 'g', 'b']
187+
# For more mutation fun!
188+
i3 = [['y', 'g'], ['b', 'k']]
189+
190+
c1 = cycler('c', i1)
191+
c2 = cycler('lw', i2)
192+
c3 = cycler('foo', i3)
193+
194+
c_before = (c1 + c2) * c3
195+
196+
i1.pop()
197+
i2.append('cyan')
198+
i3[0].append('blue')
199+
200+
c_after = (c1 + c2) * c3
201+
202+
assert_equal(c1, cycler('c', [1, 2, 3]))
203+
assert_equal(c2, cycler('lw', ['r', 'g', 'b']))
204+
assert_equal(c3, cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))
205+
assert_equal(c_before, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) *
206+
cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])))
207+
assert_equal(c_after, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) *
208+
cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])))
209+
210+
# Make sure that changing the key for a specific cycler
211+
# doesn't break things for a composed cycler
212+
c = (c1 + c2) * c3
213+
c4 = cycler('bar', c3)
214+
assert_equal(c, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) *
215+
cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])))
216+
assert_equal(c3, cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))
217+
218+
181219
def _eq_test_helper(a, b, res):
182220
if res:
183221
assert_equal(a, b)

0 commit comments

Comments
 (0)