Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f72adc4

Browse files
authored
Merge pull request #19281 from anntzer/copiable-transforms
Make all transforms copiable (and thus scales, too).
2 parents 4ad2030 + 921e945 commit f72adc4

File tree

4 files changed

+92
-5
lines changed

4 files changed

+92
-5
lines changed

lib/matplotlib/tests/test_colors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import matplotlib.colorbar as mcolorbar
1717
import matplotlib.cbook as cbook
1818
import matplotlib.pyplot as plt
19+
import matplotlib.scale as mscale
1920
from matplotlib.testing.decorators import image_comparison
2021

2122

@@ -1343,3 +1344,17 @@ def test_2d_to_rgba():
13431344
rgba_1d = mcolors.to_rgba(color.reshape(-1))
13441345
rgba_2d = mcolors.to_rgba(color.reshape((1, -1)))
13451346
assert rgba_1d == rgba_2d
1347+
1348+
1349+
def test_norm_deepcopy():
1350+
norm = mcolors.LogNorm()
1351+
norm.vmin = 0.0002
1352+
norm2 = copy.deepcopy(norm)
1353+
assert norm2.vmin == norm.vmin
1354+
assert isinstance(norm2._scale, mscale.LogScale)
1355+
norm = mcolors.Normalize()
1356+
norm.vmin = 0.0002
1357+
norm2 = copy.deepcopy(norm)
1358+
assert isinstance(norm2._scale, mscale.LinearScale)
1359+
assert norm2.vmin == norm.vmin
1360+
assert norm2._scale is not norm._scale

lib/matplotlib/tests/test_scale.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import copy
2+
13
import matplotlib.pyplot as plt
24
from matplotlib.scale import (
35
LogTransform, InvertedLogTransform,
@@ -210,3 +212,10 @@ def test_pass_scale():
210212
ax.set_yscale(scale)
211213
assert ax.xaxis.get_scale() == 'log'
212214
assert ax.yaxis.get_scale() == 'log'
215+
216+
217+
def test_scale_deepcopy():
218+
sc = mscale.LogScale(axis='x', base=10)
219+
sc2 = copy.deepcopy(sc)
220+
assert str(sc.get_transform()) == str(sc2.get_transform())
221+
assert sc._transform is not sc2._transform

lib/matplotlib/tests/test_transforms.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import copy
2+
13
import numpy as np
24
from numpy.testing import (assert_allclose, assert_almost_equal,
35
assert_array_equal, assert_array_almost_equal)
@@ -696,3 +698,41 @@ def test_lockable_bbox(locked_element):
696698
assert getattr(locked, 'locked_' + locked_element) == 3
697699
for elem in other_elements:
698700
assert getattr(locked, elem) == getattr(orig, elem)
701+
702+
703+
def test_copy():
704+
a = mtransforms.Affine2D()
705+
b = mtransforms.Affine2D()
706+
s = a + b
707+
# Updating a dependee should invalidate a copy of the dependent.
708+
s.get_matrix() # resolve it.
709+
s1 = copy.copy(s)
710+
assert not s._invalid and not s1._invalid
711+
a.translate(1, 2)
712+
assert s._invalid and s1._invalid
713+
assert (s1.get_matrix() == a.get_matrix()).all()
714+
# Updating a copy of a dependee shouldn't invalidate a dependent.
715+
s.get_matrix() # resolve it.
716+
b1 = copy.copy(b)
717+
b1.translate(3, 4)
718+
assert not s._invalid
719+
assert (s.get_matrix() == a.get_matrix()).all()
720+
721+
722+
def test_deepcopy():
723+
a = mtransforms.Affine2D()
724+
b = mtransforms.Affine2D()
725+
s = a + b
726+
# Updating a dependee shouldn't invalidate a deepcopy of the dependent.
727+
s.get_matrix() # resolve it.
728+
s1 = copy.deepcopy(s)
729+
assert not s._invalid and not s1._invalid
730+
a.translate(1, 2)
731+
assert s._invalid and not s1._invalid
732+
assert (s1.get_matrix() == mtransforms.Affine2D().get_matrix()).all()
733+
# Updating a deepcopy of a dependee shouldn't invalidate a dependent.
734+
s.get_matrix() # resolve it.
735+
b1 = copy.deepcopy(b)
736+
b1.translate(3, 4)
737+
assert not s._invalid
738+
assert (s.get_matrix() == a.get_matrix()).all()

lib/matplotlib/transforms.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# `np.minimum` instead of the builtin `min`, and likewise for `max`. This is
3434
# done so that `nan`s are propagated, instead of being silently dropped.
3535

36+
import copy
3637
import functools
3738
import textwrap
3839
import weakref
@@ -138,11 +139,33 @@ def __setstate__(self, data_dict):
138139
k: weakref.ref(v, lambda _, pop=self._parents.pop, k=k: pop(k))
139140
for k, v in self._parents.items() if v is not None}
140141

141-
def __copy__(self, *args):
142-
raise NotImplementedError(
143-
"TransformNode instances can not be copied. "
144-
"Consider using frozen() instead.")
145-
__deepcopy__ = __copy__
142+
def __copy__(self):
143+
other = copy.copy(super())
144+
# If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not
145+
# propagate back to `c`, i.e. we need to clear the parents of `a1`.
146+
other._parents = {}
147+
# If `c = a + b; c1 = copy(c)`, then modifications to `a` also need to
148+
# be propagated to `c1`.
149+
for key, val in vars(self).items():
150+
if isinstance(val, TransformNode) and id(self) in val._parents:
151+
other.set_children(val) # val == getattr(other, key)
152+
return other
153+
154+
def __deepcopy__(self, memo):
155+
# We could deepcopy the entire transform tree, but nothing except
156+
# `self` is accessible publicly, so we may as well just freeze `self`.
157+
other = self.frozen()
158+
if other is not self:
159+
return other
160+
# Some classes implement frozen() as returning self, which is not
161+
# acceptable for deepcopying, so we need to handle them separately.
162+
other = copy.deepcopy(super(), memo)
163+
# If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not
164+
# propagate back to `c`, i.e. we need to clear the parents of `a1`.
165+
other._parents = {}
166+
# If `c = a + b; c1 = copy(c)`, this creates a separate tree
167+
# (`c1 = a1 + b1`) so nothing needs to be done.
168+
return other
146169

147170
def invalidate(self):
148171
"""

0 commit comments

Comments
 (0)