Skip to content

Commit d05b43d

Browse files
authored
Merge pull request matplotlib#30198 from jkseppan/path-deepcopy-via-metaclass
Implement Path.__deepcopy__ avoiding infinite recursion
2 parents 58b6a06 + 5c7c915 commit d05b43d

File tree

4 files changed

+64
-9
lines changed

4 files changed

+64
-9
lines changed

lib/matplotlib/path.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,17 +275,37 @@ def copy(self):
275275
"""
276276
return copy.copy(self)
277277

278-
def __deepcopy__(self, memo=None):
278+
def __deepcopy__(self, memo):
279279
"""
280280
Return a deepcopy of the `Path`. The `Path` will not be
281281
readonly, even if the source `Path` is.
282282
"""
283283
# Deepcopying arrays (vertices, codes) strips the writeable=False flag.
284-
p = copy.deepcopy(super(), memo)
284+
cls = type(self)
285+
memo[id(self)] = p = cls.__new__(cls)
286+
287+
for k, v in self.__dict__.items():
288+
setattr(p, k, copy.deepcopy(v, memo))
289+
285290
p._readonly = False
286291
return p
287292

288-
deepcopy = __deepcopy__
293+
def deepcopy(self, memo=None):
294+
"""
295+
Return a deep copy of the `Path`. The `Path` will not be readonly,
296+
even if the source `Path` is.
297+
298+
Parameters
299+
----------
300+
memo : dict, optional
301+
A dictionary to use for memoizing, passed to `copy.deepcopy`.
302+
303+
Returns
304+
-------
305+
Path
306+
A deep copy of the `Path`, but not readonly.
307+
"""
308+
return copy.deepcopy(self, memo)
289309

290310
@classmethod
291311
def make_compound_path_from_polys(cls, XY):

lib/matplotlib/path.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ class Path:
4444
@property
4545
def readonly(self) -> bool: ...
4646
def copy(self) -> Path: ...
47-
def __deepcopy__(self, memo: dict[int, Any] | None = ...) -> Path: ...
48-
deepcopy = __deepcopy__
47+
def __deepcopy__(self, memo: dict[int, Any]) -> Path: ...
48+
def deepcopy(self, memo: dict[int, Any] | None = None) -> Path: ...
4949

5050
@classmethod
5151
def make_compound_path_from_polys(cls, XY: ArrayLike) -> Path: ...

lib/matplotlib/tests/test_path.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,15 +355,49 @@ def test_path_deepcopy():
355355
# Should not raise any error
356356
verts = [[0, 0], [1, 1]]
357357
codes = [Path.MOVETO, Path.LINETO]
358-
path1 = Path(verts)
359-
path2 = Path(verts, codes)
358+
path1 = Path(verts, readonly=True)
359+
path2 = Path(verts, codes, readonly=True)
360360
path1_copy = path1.deepcopy()
361361
path2_copy = path2.deepcopy()
362362
assert path1 is not path1_copy
363363
assert path1.vertices is not path1_copy.vertices
364+
assert_array_equal(path1.vertices, path1_copy.vertices)
365+
assert path1.readonly
366+
assert not path1_copy.readonly
364367
assert path2 is not path2_copy
365368
assert path2.vertices is not path2_copy.vertices
369+
assert_array_equal(path2.vertices, path2_copy.vertices)
366370
assert path2.codes is not path2_copy.codes
371+
assert_array_equal(path2.codes, path2_copy.codes)
372+
assert path2.readonly
373+
assert not path2_copy.readonly
374+
375+
376+
def test_path_deepcopy_cycle():
377+
class PathWithCycle(Path):
378+
def __init__(self, *args, **kwargs):
379+
super().__init__(*args, **kwargs)
380+
self.x = self
381+
382+
p = PathWithCycle([[0, 0], [1, 1]], readonly=True)
383+
p_copy = p.deepcopy()
384+
assert p_copy is not p
385+
assert p.readonly
386+
assert not p_copy.readonly
387+
assert p_copy.x is p_copy
388+
389+
class PathWithCycle2(Path):
390+
def __init__(self, *args, **kwargs):
391+
super().__init__(*args, **kwargs)
392+
self.x = [self] * 2
393+
394+
p2 = PathWithCycle2([[0, 0], [1, 1]], readonly=True)
395+
p2_copy = p2.deepcopy()
396+
assert p2_copy is not p2
397+
assert p2.readonly
398+
assert not p2_copy.readonly
399+
assert p2_copy.x[0] is p2_copy
400+
assert p2_copy.x[1] is p2_copy
367401

368402

369403
def test_path_shallowcopy():

lib/matplotlib/transforms.py

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

38-
import copy
3938
import functools
4039
import itertools
4140
import textwrap
@@ -139,7 +138,9 @@ def __setstate__(self, data_dict):
139138
for k, v in self._parents.items() if v is not None}
140139

141140
def __copy__(self):
142-
other = copy.copy(super())
141+
cls = type(self)
142+
other = cls.__new__(cls)
143+
other.__dict__.update(self.__dict__)
143144
# If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not
144145
# propagate back to `c`, i.e. we need to clear the parents of `a1`.
145146
other._parents = {}

0 commit comments

Comments
 (0)