From d950e5a52c23ef7ed400303bf129e8affa53236d Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Fri, 27 Aug 2021 08:27:02 +0200 Subject: [PATCH 1/6] Fix problem with (deep)copy of TextPath The problem is that deepcopy of TextPath calls deepcopy of Path. In turn `Path` utilizes `super().__init__` for creating a new instance. However, `Path.__init__ and TextPath.__init__ are completely different. Moreover, `TextPath.__init__` converts some arguments directly to list of vertices, hence it is not possible to create a new instance of `TextPath` from its members. Additionally, there is a problem with caching. When copying, the vertices can be copied uncached, which causes discrepancies between members of copy and original members (recaching creates new members). Therefore validation of cache was removed and new vertices are generated at every size set. --- lib/matplotlib/path.py | 2 +- lib/matplotlib/tests/test_textpath.py | 37 +++++++++++++++++++++++++++ lib/matplotlib/textpath.py | 25 ++++++++---------- 3 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 lib/matplotlib/tests/test_textpath.py diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 4280d55eeacd..09150a9e741f 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -276,7 +276,7 @@ def __deepcopy__(self, memo=None): codes = self.codes.copy() except AttributeError: codes = None - return self.__class__( + return Path( self.vertices.copy(), codes, _interpolation_steps=self._interpolation_steps) diff --git a/lib/matplotlib/tests/test_textpath.py b/lib/matplotlib/tests/test_textpath.py new file mode 100644 index 000000000000..ac084733063e --- /dev/null +++ b/lib/matplotlib/tests/test_textpath.py @@ -0,0 +1,37 @@ +from copy import copy, deepcopy + +from numpy.testing import assert_array_equal +from matplotlib.textpath import TextPath + + +def test_set_size(): + path = TextPath((0, 0), ".") + _size = path.get_size() + verts = path.vertices.copy() + codes = path.codes.copy() + path.set_size(20) + assert_array_equal(verts/_size*path.get_size(), path.vertices) + assert_array_equal(codes, path.codes) + + +def test_deepcopy(): + # Should not raise any error + path = TextPath((0, 0), ".") + path_copy = deepcopy(path) + assert path is not path_copy + assert path.vertices is not path_copy.vertices + assert path.codes is not path_copy.codes + + +def test_copy(): + # Should not raise any error + path = TextPath((0, 0), ".") + path_copy = copy(path) + assert path is not path_copy + assert path.vertices is path_copy.vertices + assert path.codes is path_copy.codes + path = TextPath((0, 0), ".") + path_copy = path.copy() + assert path is not path_copy + assert path.vertices is path_copy.vertices + assert path.codes is path_copy.codes diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 9b14e79ec2d2..5c04dca5aa95 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -381,7 +381,6 @@ def __init__(self, xy, s, size=None, prop=None, size = prop.get_size_in_points() self._xy = xy - self.set_size(size) self._cached_vertices = None s, ismath = Text(usetex=usetex)._preprocess_math(s) @@ -390,11 +389,12 @@ def __init__(self, xy, s, size=None, prop=None, _interpolation_steps=_interpolation_steps, readonly=True) self._should_simplify = False + self.set_size(size) def set_size(self, size): """Set the text size.""" self._size = size - self._invalid = True + self._recache_path() def get_size(self): """Get the text size.""" @@ -403,9 +403,8 @@ def get_size(self): @property def vertices(self): """ - Return the cached path after updating it if necessary. + Return the cached path. """ - self._revalidate_path() return self._cached_vertices @property @@ -415,17 +414,15 @@ def codes(self): """ return self._codes - def _revalidate_path(self): + def _recache_path(self): """ - Update the path if necessary. + Update the path. - The path for the text is initially create with the font size of + The path for the text is initially created with the font size of `.FONT_SCALE`, and this path is rescaled to other size when necessary. """ - if self._invalid or self._cached_vertices is None: - tr = (Affine2D() - .scale(self._size / text_to_path.FONT_SCALE) - .translate(*self._xy)) - self._cached_vertices = tr.transform(self._vertices) - self._cached_vertices.flags.writeable = False - self._invalid = False + tr = (Affine2D() + .scale(self._size / text_to_path.FONT_SCALE) + .translate(*self._xy)) + self._cached_vertices = tr.transform(self._vertices) + self._cached_vertices.flags.writeable = False From 6db9e951309e5c2a98679ab3760d939d85762c8c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Aug 2021 15:32:16 -0400 Subject: [PATCH 2/6] TST: deepcopy should return the same type --- lib/matplotlib/tests/test_textpath.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_textpath.py b/lib/matplotlib/tests/test_textpath.py index ac084733063e..d9f8d933ceea 100644 --- a/lib/matplotlib/tests/test_textpath.py +++ b/lib/matplotlib/tests/test_textpath.py @@ -18,6 +18,7 @@ def test_deepcopy(): # Should not raise any error path = TextPath((0, 0), ".") path_copy = deepcopy(path) + assert isinstance(path_copy, TextPath) assert path is not path_copy assert path.vertices is not path_copy.vertices assert path.codes is not path_copy.codes From c2785a1fd33122a233b940a3f2d39df76bf52b59 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 29 Aug 2021 19:58:58 +0200 Subject: [PATCH 3/6] Revert "Fix problem with (deep)copy of TextPath" This reverts commit 7c152aab3bd62e440390046c59274f4c3b9c5e14. --- lib/matplotlib/path.py | 2 +- lib/matplotlib/textpath.py | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 09150a9e741f..4280d55eeacd 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -276,7 +276,7 @@ def __deepcopy__(self, memo=None): codes = self.codes.copy() except AttributeError: codes = None - return Path( + return self.__class__( self.vertices.copy(), codes, _interpolation_steps=self._interpolation_steps) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 5c04dca5aa95..9b14e79ec2d2 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -381,6 +381,7 @@ def __init__(self, xy, s, size=None, prop=None, size = prop.get_size_in_points() self._xy = xy + self.set_size(size) self._cached_vertices = None s, ismath = Text(usetex=usetex)._preprocess_math(s) @@ -389,12 +390,11 @@ def __init__(self, xy, s, size=None, prop=None, _interpolation_steps=_interpolation_steps, readonly=True) self._should_simplify = False - self.set_size(size) def set_size(self, size): """Set the text size.""" self._size = size - self._recache_path() + self._invalid = True def get_size(self): """Get the text size.""" @@ -403,8 +403,9 @@ def get_size(self): @property def vertices(self): """ - Return the cached path. + Return the cached path after updating it if necessary. """ + self._revalidate_path() return self._cached_vertices @property @@ -414,15 +415,17 @@ def codes(self): """ return self._codes - def _recache_path(self): + def _revalidate_path(self): """ - Update the path. + Update the path if necessary. - The path for the text is initially created with the font size of + The path for the text is initially create with the font size of `.FONT_SCALE`, and this path is rescaled to other size when necessary. """ - tr = (Affine2D() - .scale(self._size / text_to_path.FONT_SCALE) - .translate(*self._xy)) - self._cached_vertices = tr.transform(self._vertices) - self._cached_vertices.flags.writeable = False + if self._invalid or self._cached_vertices is None: + tr = (Affine2D() + .scale(self._size / text_to_path.FONT_SCALE) + .translate(*self._xy)) + self._cached_vertices = tr.transform(self._vertices) + self._cached_vertices.flags.writeable = False + self._invalid = False From c633e33abf0d0e51cd048830244c1f576e136c65 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Sun, 29 Aug 2021 20:32:38 +0200 Subject: [PATCH 4/6] Cryptic (deep)copy of TextPath This time, revalidation is enabled. Added test for member deepcopy(). --- lib/matplotlib/tests/test_textpath.py | 8 ++++++-- lib/matplotlib/textpath.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_textpath.py b/lib/matplotlib/tests/test_textpath.py index d9f8d933ceea..5ef0f7b8216d 100644 --- a/lib/matplotlib/tests/test_textpath.py +++ b/lib/matplotlib/tests/test_textpath.py @@ -15,17 +15,21 @@ def test_set_size(): def test_deepcopy(): - # Should not raise any error path = TextPath((0, 0), ".") path_copy = deepcopy(path) assert isinstance(path_copy, TextPath) assert path is not path_copy assert path.vertices is not path_copy.vertices assert path.codes is not path_copy.codes + path = TextPath((0, 0), ".") + path_copy = path.deepcopy({}) + assert isinstance(path_copy, TextPath) + assert path is not path_copy + assert path.vertices is not path_copy.vertices + assert path.codes is not path_copy.codes def test_copy(): - # Should not raise any error path = TextPath((0, 0), ".") path_copy = copy(path) assert path is not path_copy diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 9b14e79ec2d2..da39bb8f24d3 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -1,3 +1,4 @@ +from copy import deepcopy from collections import OrderedDict import functools import logging @@ -429,3 +430,23 @@ def _revalidate_path(self): self._cached_vertices = tr.transform(self._vertices) self._cached_vertices.flags.writeable = False self._invalid = False + + def __deepcopy__(self, memo): + # taken from https://stackoverflow.com/a/15774013 + self._revalidate_path() + cls = self.__class__ + new_instance = cls.__new__(cls) + memo[id(self)] = new_instance + for k, v in self.__dict__.items(): + setattr(new_instance, k, deepcopy(v, memo)) + return new_instance + + deepcopy = __deepcopy__ + + def __copy__(self): + # taken from https://stackoverflow.com/a/15774013 + self._revalidate_path() + cls = self.__class__ + new_instance = cls.__new__(cls) + new_instance.__dict__.update(self.__dict__) + return new_instance From 065528ca1e246ac3bba4f4764383cd82e277c96f Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Tue, 31 Aug 2021 14:15:34 +0200 Subject: [PATCH 5/6] Update docstrings of TextPath copy, deepcopy --- lib/matplotlib/textpath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index da39bb8f24d3..c46c3d2a3e1b 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -432,7 +432,7 @@ def _revalidate_path(self): self._invalid = False def __deepcopy__(self, memo): - # taken from https://stackoverflow.com/a/15774013 + """Update path and create deep copy.""" self._revalidate_path() cls = self.__class__ new_instance = cls.__new__(cls) @@ -444,7 +444,7 @@ def __deepcopy__(self, memo): deepcopy = __deepcopy__ def __copy__(self): - # taken from https://stackoverflow.com/a/15774013 + """Update path and create shallow copy.""" self._revalidate_path() cls = self.__class__ new_instance = cls.__new__(cls) From ebebec0169711896cd15e55dcdc248c9f2c4fd74 Mon Sep 17 00:00:00 2001 From: Jakub Klus Date: Tue, 21 Sep 2021 23:49:45 +0200 Subject: [PATCH 6/6] Add rudimentary docstring to test_set_size(). --- lib/matplotlib/tests/test_textpath.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_textpath.py b/lib/matplotlib/tests/test_textpath.py index 5ef0f7b8216d..dcc3f80f4a4b 100644 --- a/lib/matplotlib/tests/test_textpath.py +++ b/lib/matplotlib/tests/test_textpath.py @@ -5,6 +5,7 @@ def test_set_size(): + """Set size with zero offset should scale vertices and retain codes.""" path = TextPath((0, 0), ".") _size = path.get_size() verts = path.vertices.copy()