Skip to content

Implement Path.__deepcopy__ avoiding infinite recursion #30198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
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_array_equal(path1.vertices, path1_copy.vertices)
assert path1.readonly
assert not path1_copy.readonly
assert path2 is not path2_copy
assert path2.vertices is not path2_copy.vertices
assert_array_equal(path2.vertices, path2_copy.vertices)
assert path2.codes is not path2_copy.codes
assert_array_equal(path2.codes, path2_copy.codes)
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
5 changes: 3 additions & 2 deletions lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
# `np.minimum` instead of the builtin `min`, and likewise for `max`. This is
# done so that `nan`s are propagated, instead of being silently dropped.

import copy
import functools
import itertools
import textwrap
Expand Down Expand Up @@ -139,7 +138,9 @@ def __setstate__(self, data_dict):
for k, v in self._parents.items() if v is not None}

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