Skip to content

Commit c7edce3

Browse files
Implement Path.__deepcopy__ avoiding infinite recursion
To deep copy an object without calling deepcopy on the object itself, create a new object of the correct class and iterate calling deepcopy on its __dict__. Closes #29157 without relying on private CPython methods. Does not fix the other issue with TransformNode.__copy__. Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent fed8c20 commit c7edce3

File tree

3 files changed

+61
-7
lines changed

3 files changed

+61
-7
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 np.all(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 np.all(path2.vertices == path2_copy.vertices)
366370
assert path2.codes is not path2_copy.codes
371+
assert all(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():

0 commit comments

Comments
 (0)