diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 3fa3fb262e27..aef2e9044c6a 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -276,17 +276,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): diff --git a/lib/matplotlib/path.pyi b/lib/matplotlib/path.pyi index 464fc6d9a912..8a5a5c03792e 100644 --- a/lib/matplotlib/path.pyi +++ b/lib/matplotlib/path.pyi @@ -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: ... diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index 21f4c33794af..1ec6209bbbb4 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -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(): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 2934b0a77809..ca04e729df4f 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -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 @@ -141,7 +140,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 = {}