diff --git a/doc/api/next_api_changes/deprecations/17737-AL.rst b/doc/api/next_api_changes/deprecations/17737-AL.rst new file mode 100644 index 000000000000..1757a8563639 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/17737-AL.rst @@ -0,0 +1,6 @@ +BoxStyles are now called without passing the *mutation_aspect* parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Mutation aspect is now handled by the artist itself. Hence the +*mutation_aspect* parameter of ``BoxStyle._Base.__call__`` is deprecated, and +custom boxstyles should be implemented to not require this parameter (it can be +left as a parameter defaulting to 1 for back-compatibility). diff --git a/examples/userdemo/custom_boxstyle01.py b/examples/userdemo/custom_boxstyle01.py index 36299afa8f91..5ca0a21b59fb 100644 --- a/examples/userdemo/custom_boxstyle01.py +++ b/examples/userdemo/custom_boxstyle01.py @@ -25,7 +25,7 @@ # ``bbox=dict(boxstyle=custom_box_style, ...)`` to `.Axes.text`. -def custom_box_style(x0, y0, width, height, mutation_size, mutation_aspect=1): +def custom_box_style(x0, y0, width, height, mutation_size): """ Given the location and size of the box, return the path of the box around it. @@ -38,11 +38,7 @@ def custom_box_style(x0, y0, width, height, mutation_size, mutation_aspect=1): Box location and size. mutation_size : float Mutation reference scale, typically the text font size. - mutation_aspect - Mutation aspect ratio. """ - # We ignore mutation_aspect. This is okay in general. - # padding mypad = 0.3 pad = mutation_size * mypad @@ -66,19 +62,17 @@ def custom_box_style(x0, y0, width, height, mutation_size, mutation_aspect=1): ############################################################################### -# Alternatively, custom box styles can be implemented as subclasses of -# ``matplotlib.patches.BoxStyle._Base``, by overriding the ``transmute`` -# method, as demonstrated below. +# Likewise, custom box styles can be implemented as classes that implement +# ``__call__``. # -# The subclass can then be registered into the ``BoxStyle._style_list`` dict, +# The classes can then be registered into the ``BoxStyle._style_list`` dict, # which allows specifying the box style as a string, # ``bbox=dict(boxstyle="registered_name,param=value,...", ...)``. -# -# Note that this approach relies on internal APIs and is therefore not +# Note that this registration relies on internal APIs and is therefore not # officially supported. -class MyStyle(BoxStyle._Base): +class MyStyle: """A simple box.""" def __init__(self, pad=0.3): @@ -93,7 +87,7 @@ def __init__(self, pad=0.3): self.pad = pad super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): """ Given the location and size of the box, return the path of the box around it. @@ -106,14 +100,6 @@ def transmute(self, x0, y0, width, height, mutation_size): Box location and size. mutation_size : float Reference scale for the mutation, typically the text font size. - - Notes - ----- - Unlike when defining the box style as a function (as in - `custom_box_style`), here there is no *mutation_aspect* parameter. - Matplotlib will first squeeze the box's y-axis by *mutation_aspect* - before calling the `transmute` method, and then later reexpand the - y-axis by the same amount. """ # padding pad = mutation_size * self.pad diff --git a/lib/matplotlib/cbook/deprecation.py b/lib/matplotlib/cbook/deprecation.py index d84709fb9373..e7f0ec6f1a10 100644 --- a/lib/matplotlib/cbook/deprecation.py +++ b/lib/matplotlib/cbook/deprecation.py @@ -465,7 +465,8 @@ def _deprecate_method_override(method, obj, *, allow_empty=False, **kwargs): can always use ``__class__`` to refer to the class that is currently being defined. obj - An object of the class where *method* is defined. + Either an object of the class where *method* is defined, or a subclass + of that class. allow_empty : bool, default: False Whether to allow overrides by "empty" methods without emitting a warning. @@ -478,15 +479,19 @@ def empty(): pass def empty_with_docstring(): """doc""" name = method.__name__ - bound_method = getattr(obj, name) - if (bound_method != method.__get__(obj) + bound_child = getattr(obj, name) + bound_base = ( + method # If obj is a class, then we need to use unbound methods. + if isinstance(bound_child, type(empty)) and isinstance(obj, type) + else method.__get__(obj)) + if (bound_child != bound_base and (not allow_empty - or (getattr(getattr(bound_method, "__code__", None), + or (getattr(getattr(bound_child, "__code__", None), "co_code", None) not in [empty.__code__.co_code, empty_with_docstring.__code__.co_code]))): warn_deprecated(**{"name": name, "obj_type": "method", **kwargs}) - return bound_method + return bound_child return None diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 859d495111cb..e1eb039d9819 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1917,18 +1917,18 @@ class BoxStyle(_Style): An instance of any boxstyle class is an callable object, whose call signature is:: - __call__(self, x0, y0, width, height, mutation_size, aspect_ratio=1.) + __call__(self, x0, y0, width, height, mutation_size) and returns a `.Path` instance. *x0*, *y0*, *width* and *height* specify the location and size of the box to be drawn. *mutation_scale* determines the overall size of the mutation (by which I mean the transformation of the rectangle to - the fancy box). *mutation_aspect* determines the aspect-ratio of - the mutation. + the fancy box). """ _style_list = {} + @cbook.deprecated("3.4") class _Base: """ Abstract base class for styling of `.FancyBboxPatch`. @@ -1941,18 +1941,48 @@ class _Base: ``__init__`` method because they must be able to be initialized without arguments. - Subclasses must implement the `transmute` method. It receives the + Subclasses must implement the `__call__` method. It receives the enclosing rectangle *x0, y0, width, height* as well as the *mutation_size*, which scales the outline properties such as padding. It returns the outline of the fancy box as `.path.Path`. """ + @cbook.deprecated("3.4") def transmute(self, x0, y0, width, height, mutation_size): """Return the `~.path.Path` outlining the given rectangle.""" - raise NotImplementedError('Derived must override') - - def __call__(self, x0, y0, width, height, mutation_size, - aspect_ratio=1.): + return self(self, x0, y0, width, height, mutation_size, 1) + + # This can go away once the deprecation period elapses, leaving _Base + # as a fully abstract base class just providing docstrings, no logic. + def __init_subclass__(cls): + transmute = cbook._deprecate_method_override( + __class__.transmute, cls, since="3.4") + if transmute: + cls.__call__ = transmute + return + + __call__ = cls.__call__ + + @cbook._delete_parameter("3.4", "mutation_aspect") + def call_wrapper( + self, x0, y0, width, height, mutation_size, + mutation_aspect=cbook.deprecation._deprecated_parameter): + if mutation_aspect is cbook.deprecation._deprecated_parameter: + # Don't trigger deprecation warning internally. + return __call__(self, x0, y0, width, height, mutation_size) + else: + # Squeeze the given height by the aspect_ratio. + y0, height = y0 / mutation_aspect, height / mutation_aspect + path = self(x0, y0, width, height, mutation_size, + mutation_aspect) + vertices, codes = path.vertices, path.codes + # Restore the height. + vertices[:, 1] = vertices[:, 1] * mutation_aspect + return Path(vertices, codes) + + cls.__call__ = call_wrapper + + def __call__(self, x0, y0, width, height, mutation_size): """ Given the location and size of the box, return the path of the box around it. @@ -1963,43 +1993,27 @@ def __call__(self, x0, y0, width, height, mutation_size, Location and size of the box. mutation_size : float A reference scale for the mutation. - aspect_ratio : float, default: 1 - Aspect-ratio for the mutation. Returns ------- `~matplotlib.path.Path` """ - # The __call__ method is a thin wrapper around the transmute method - # and takes care of the aspect. - - if aspect_ratio is not None: - # Squeeze the given height by the aspect_ratio - y0, height = y0 / aspect_ratio, height / aspect_ratio - # call transmute method with squeezed height. - path = self.transmute(x0, y0, width, height, mutation_size) - vertices, codes = path.vertices, path.codes - # Restore the height - vertices[:, 1] = vertices[:, 1] * aspect_ratio - return Path(vertices, codes) - else: - return self.transmute(x0, y0, width, height, mutation_size) + raise NotImplementedError('Derived must override') @_register_style(_style_list) class Square(_Base): - """ - A square box. + """A square box.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ def __init__(self, pad=0.3): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + """ self.pad = pad - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): pad = mutation_size * self.pad # width and height with padding added. width, height = width + 2 * pad, height + 2 * pad @@ -2011,19 +2025,18 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class Circle(_Base): - """ - A circular box. + """A circular box.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ def __init__(self, pad=0.3): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + """ self.pad = pad - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): pad = mutation_size * self.pad width, height = width + 2 * pad, height + 2 * pad # boundary of the padded box @@ -2033,19 +2046,18 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class LArrow(_Base): - """ - A box in the shape of a left-pointing arrow. + """A box in the shape of a left-pointing arrow.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ def __init__(self, pad=0.3): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + """ self.pad = pad - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): # padding pad = mutation_size * self.pad # width and height with padding added. @@ -2066,41 +2078,29 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class RArrow(LArrow): - """ - A box in the shape of a right-pointing arrow. - - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ - def __init__(self, pad=0.3): - super().__init__(pad) + """A box in the shape of a right-pointing arrow.""" - def transmute(self, x0, y0, width, height, mutation_size): - p = BoxStyle.LArrow.transmute(self, x0, y0, - width, height, mutation_size) + def __call__(self, x0, y0, width, height, mutation_size): + p = BoxStyle.LArrow.__call__( + self, x0, y0, width, height, mutation_size) p.vertices[:, 0] = 2 * x0 + width - p.vertices[:, 0] return p @_register_style(_style_list) class DArrow(_Base): - """ - A box in the shape of a two-way arrow. - - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - """ - # This source is copied from LArrow, - # modified to add a right arrow to the bbox. + """A box in the shape of a two-way arrow.""" + # Modified from LArrow to add a right arrow to the bbox. def __init__(self, pad=0.3): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + """ self.pad = pad - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): # padding pad = mutation_size * self.pad # width and height with padding added. @@ -2125,22 +2125,21 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class Round(_Base): - """ - A box with round corners. + """A box with round corners.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - rounding_size : float, default: *pad* - Radius of the corners. - """ def __init__(self, pad=0.3, rounding_size=None): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + rounding_size : float, default: *pad* + Radius of the corners. + """ self.pad = pad self.rounding_size = rounding_size - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): # padding pad = mutation_size * self.pad @@ -2186,22 +2185,21 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class Round4(_Base): - """ - A box with rounded edges. + """A box with rounded edges.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - rounding_size : float, default: *pad*/2 - Rounding of edges. - """ def __init__(self, pad=0.3, rounding_size=None): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + rounding_size : float, default: *pad*/2 + Rounding of edges. + """ self.pad = pad self.rounding_size = rounding_size - super().__init__() - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): # padding pad = mutation_size * self.pad @@ -2238,20 +2236,19 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class Sawtooth(_Base): - """ - A box with a sawtooth outline. + """A box with a sawtooth outline.""" - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - tooth_size : float, default: *pad*/2 - Size of the sawtooth. - """ def __init__(self, pad=0.3, tooth_size=None): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + tooth_size : float, default: *pad*/2 + Size of the sawtooth. + """ self.pad = pad self.tooth_size = tooth_size - super().__init__() def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size): @@ -2327,7 +2324,7 @@ def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size): return saw_vertices - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): saw_vertices = self._get_sawtooth_vertices(x0, y0, width, height, mutation_size) path = Path(saw_vertices, closed=True) @@ -2335,20 +2332,9 @@ def transmute(self, x0, y0, width, height, mutation_size): @_register_style(_style_list) class Roundtooth(Sawtooth): - """ - A box with a rounded sawtooth outline. - - Parameters - ---------- - pad : float, default: 0.3 - The amount of padding around the original box. - tooth_size : float, default: *pad*/2 - Size of the sawtooth. - """ - def __init__(self, pad=0.3, tooth_size=None): - super().__init__(pad, tooth_size) + """A box with a rounded sawtooth outline.""" - def transmute(self, x0, y0, width, height, mutation_size): + def __call__(self, x0, y0, width, height, mutation_size): saw_vertices = self._get_sawtooth_vertices(x0, y0, width, height, mutation_size) @@ -3718,11 +3704,31 @@ def get_boxstyle(self): def get_path(self): """Return the mutated path of the rectangle.""" - _path = self.get_boxstyle()(self._x, self._y, - self._width, self._height, - self.get_mutation_scale(), - self.get_mutation_aspect()) - return _path + boxstyle = self.get_boxstyle() + x = self._x + y = self._y + width = self._width + height = self._height + m_scale = self.get_mutation_scale() + m_aspect = self.get_mutation_aspect() + # Squeeze the given height by the aspect_ratio. + y, height = y / m_aspect, height / m_aspect + # Call boxstyle with squeezed height. + try: + inspect.signature(boxstyle).bind(x, y, width, height, m_scale) + except TypeError: + # Don't apply aspect twice. + path = boxstyle(x, y, width, height, m_scale, 1) + cbook.warn_deprecated( + "3.4", message="boxstyles must be callable without the " + "'mutation_aspect' parameter since %(since)s; support for the " + "old call signature will be removed %(removal)s.") + else: + path = boxstyle(x, y, width, height, m_scale) + vertices, codes = path.vertices, path.codes + # Restore the height. + vertices[:, 1] = vertices[:, 1] * m_aspect + return Path(vertices, codes) # Following methods are borrowed from the Rectangle class.