From 996b6475d8174732ece7aed85a115c41a18ab0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jun 2025 09:56:00 +0300 Subject: [PATCH 1/4] Implement Path.__deepcopy__ avoiding infinite recursion Give it a metaclass that lets us remove the __deepcopy__ method from sight when executing that method. Closes #29157 without relying on private CPython methods. Does not fix the other issue with TransformNode.__copy__. --- lib/matplotlib/path.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index a021706fb1e5..3b4080158b0d 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -16,12 +16,38 @@ import numpy as np import matplotlib as mpl + from . import _api, _path from .cbook import _to_unmasked_float_array, simple_linear_interpolation from .bezier import BezierSegment -class Path: +class _HideDeepcopyMeta(type): + """Metaclass that allows conditionally hiding the __deepcopy__ method. + + Set __hide_deepcopy__ to True to hide the __deepcopy__ method, + which will then be looked up in the usual method resolution order. + """ + + def __new__(cls, name, bases, namespace): + orig_ga = namespace.get("__getattribute__") or object.__getattribute__ + + def __getattribute__(self, attr_name): + if attr_name == "__deepcopy__" and orig_ga(self, "__hide_deepcopy__"): + for base in type(self).__mro__[1:]: + if attr_name in base.__dict__: + method = base.__dict__[attr_name] + return method.__get__(self, type(self)) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{attr_name}'" + ) + return orig_ga(self, attr_name) + + namespace["__getattribute__"] = __getattribute__ + return super().__new__(cls, name, bases, namespace) + + +class Path(metaclass=_HideDeepcopyMeta): """ A series of possibly disconnected, possibly closed, line and curve segments. @@ -281,9 +307,13 @@ def __deepcopy__(self, memo=None): readonly, even if the source `Path` is. """ # Deepcopying arrays (vertices, codes) strips the writeable=False flag. - p = copy.deepcopy(super(), memo) - p._readonly = False - return p + self.__hide_deepcopy__ = True + try: + p = copy.deepcopy(self, memo) + p._readonly = False + return p + finally: + self.__hide_deepcopy__ = False deepcopy = __deepcopy__ From 68d25b3ee3692d66dc876ba6b169044c48e2242f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jun 2025 10:36:47 +0300 Subject: [PATCH 2/4] Add metaclass to stub file --- lib/matplotlib/path.pyi | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/path.pyi b/lib/matplotlib/path.pyi index 464fc6d9a912..0cc8b9d04f36 100644 --- a/lib/matplotlib/path.pyi +++ b/lib/matplotlib/path.pyi @@ -7,7 +7,11 @@ from numpy.typing import ArrayLike from typing import Any, overload -class Path: +class _HideDeepcopyMeta(type): + def __new__(cls, name, bases, namespace): + ... + +class Path(metaclass=_HideDeepcopyMeta): code_type: type[np.uint8] STOP: np.uint8 MOVETO: np.uint8 From 990ffdfd89e97b1eda70b51d521d53651a2f180f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jun 2025 17:18:38 +0300 Subject: [PATCH 3/4] Fix TransformNode.__copy__ without calling copy.copy --- lib/matplotlib/transforms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 7228f05bcf9e..350113c56170 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 @@ -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 = {} From 8258353e0f8549da63a6f2d4e92d7fe34a9de919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jun 2025 21:43:43 +0300 Subject: [PATCH 4/4] Simplify the metaclass No reason to look up __deepcopy__ in base classes. --- lib/matplotlib/path.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 3b4080158b0d..e959c5c7f143 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -25,8 +25,7 @@ class _HideDeepcopyMeta(type): """Metaclass that allows conditionally hiding the __deepcopy__ method. - Set __hide_deepcopy__ to True to hide the __deepcopy__ method, - which will then be looked up in the usual method resolution order. + Set __hide_deepcopy__ to True to hide the __deepcopy__ method. """ def __new__(cls, name, bases, namespace): @@ -34,10 +33,6 @@ def __new__(cls, name, bases, namespace): def __getattribute__(self, attr_name): if attr_name == "__deepcopy__" and orig_ga(self, "__hide_deepcopy__"): - for base in type(self).__mro__[1:]: - if attr_name in base.__dict__: - method = base.__dict__[attr_name] - return method.__get__(self, type(self)) raise AttributeError( f"'{type(self).__name__}' object has no attribute '{attr_name}'" )