Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
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>
  • Loading branch information
jkseppan and serhiy-storchaka committed Jun 25, 2025
commit c7edce3820d918474fadb42e55818d1c40be1979
26 changes: 23 additions & 3 deletions lib/matplotlib/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,37 @@ def copy(self):
"""
return copy.copy(self)

def __deepcopy__(self, memo=None):
def __deepcopy__(self, memo):
"""
Return a deepcopy of the `Path`. The `Path` will not be
readonly, even if the source `Path` is.
"""
# Deepcopying arrays (vertices, codes) strips the writeable=False flag.
p = copy.deepcopy(super(), memo)
cls = type(self)
memo[id(self)] = p = cls.__new__(cls)

for k, v in self.__dict__.items():
setattr(p, k, copy.deepcopy(v, memo))

p._readonly = False
return p

deepcopy = __deepcopy__
def deepcopy(self, memo=None):
"""
Return a deep copy of the `Path`. The `Path` will not be readonly,
even if the source `Path` is.

Parameters
----------
memo : dict, optional
A dictionary to use for memoizing, passed to `copy.deepcopy`.

Returns
-------
Path
A deep copy of the `Path`, but not readonly.
"""
return copy.deepcopy(self, memo)

@classmethod
def make_compound_path_from_polys(cls, XY):
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/path.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class Path:
@property
def readonly(self) -> bool: ...
def copy(self) -> Path: ...
def __deepcopy__(self, memo: dict[int, Any] | None = ...) -> Path: ...
deepcopy = __deepcopy__
def __deepcopy__(self, memo: dict[int, Any]) -> Path: ...
def deepcopy(self, memo: dict[int, Any] | None = None) -> Path: ...

@classmethod
def make_compound_path_from_polys(cls, XY: ArrayLike) -> Path: ...
Expand Down
38 changes: 36 additions & 2 deletions lib/matplotlib/tests/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,49 @@ def test_path_deepcopy():
# Should not raise any error
verts = [[0, 0], [1, 1]]
codes = [Path.MOVETO, Path.LINETO]
path1 = Path(verts)
path2 = Path(verts, codes)
path1 = Path(verts, readonly=True)
path2 = Path(verts, codes, readonly=True)
path1_copy = path1.deepcopy()
path2_copy = path2.deepcopy()
assert path1 is not path1_copy
assert path1.vertices is not path1_copy.vertices
assert np.all(path1.vertices == path1_copy.vertices)
Comment thread
jkseppan marked this conversation as resolved.
Outdated
assert path1.readonly
assert not path1_copy.readonly
assert path2 is not path2_copy
assert path2.vertices is not path2_copy.vertices
assert np.all(path2.vertices == path2_copy.vertices)
Comment thread
jkseppan marked this conversation as resolved.
Outdated
assert path2.codes is not path2_copy.codes
assert all(path2.codes == path2_copy.codes)
Comment thread
jkseppan marked this conversation as resolved.
Outdated
assert path2.readonly
assert not path2_copy.readonly


def test_path_deepcopy_cycle():
class PathWithCycle(Path):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = self

p = PathWithCycle([[0, 0], [1, 1]], readonly=True)
p_copy = p.deepcopy()
assert p_copy is not p
assert p.readonly
assert not p_copy.readonly
assert p_copy.x is p_copy

class PathWithCycle2(Path):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.x = [self] * 2

p2 = PathWithCycle2([[0, 0], [1, 1]], readonly=True)
p2_copy = p2.deepcopy()
assert p2_copy is not p2
assert p2.readonly
assert not p2_copy.readonly
assert p2_copy.x[0] is p2_copy
assert p2_copy.x[1] is p2_copy


def test_path_shallowcopy():
Expand Down