From 6e1acad8f4fae1318d0abf48d7742846b83711f3 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:32:47 +0100 Subject: [PATCH 1/2] MNT: Registered 3rd party scales do not need an axis parameter anymore First step of #29349. --- .../deprecations/29358-TH.rst | 17 ++++ lib/matplotlib/scale.py | 50 ++++++++++-- lib/matplotlib/tests/test_scale.py | 78 ++++++++++++++++++- 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/29358-TH.rst diff --git a/doc/api/next_api_changes/deprecations/29358-TH.rst b/doc/api/next_api_changes/deprecations/29358-TH.rst new file mode 100644 index 000000000000..baa0435a7bf1 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29358-TH.rst @@ -0,0 +1,17 @@ +3rd party scales do not need to have an *axis* parameter anymore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since matplotlib 3.1 `PR 12831 `_ +scale objects should be reusable and therefore independent of any particular Axis. +Therefore, the use of of the *axis* parameter in the ``__init__`` had been discouraged. +However, having that parameter in the signature was still necessary for API +backwards-compatibility. This is no longer the case. + +`.register_scale` now accepts scale classes with or without this parameter. + +The *axis* parameter is pending-deprecated. It will be deprecated in matplotlib 3.13, +and removed in matplotlib 3.15. + +3rd-party scales are recommended to remove the *axis* parameter now if they can +afford to restrict compatibility to matplotlib >= 3.11 already. Otherwise, they may +keep the *axis* parameter and remove it in time for matplotlib 3.13. diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 1553e057670f..f6ccc42442d6 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -77,10 +77,18 @@ def __init__(self, axis): For back-compatibility reasons, scales take an `~matplotlib.axis.Axis` object as the first argument. - The current recommendation for `.ScaleBase` subclasses is to have the - *axis* parameter for API compatibility, but not make use of it. This is - because we plan to remove this argument to make a scale object usable - by multiple `~matplotlib.axis.Axis`\es at the same time. + .. deprecated:: 3.11 + + The *axis* parameter is now optional, i.e. matplotlib is compatible + with `.ScaleBase` subclasses that do not take an *axis* parameter. + + The *axis* parameter is pending-deprecated. It will be deprecated + in matplotlib 3.13, and removed in matplotlib 3.15. + + 3rd-party scales are recommended to remove the *axis* parameter now + if they can afford to restrict compatibility to matplotlib >= 3.11 + already. Otherwise, they may keep the *axis* parameter and remove it + in time for matplotlib 3.13. """ def get_transform(self): @@ -801,6 +809,20 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'functionlog': FuncScaleLog, } +# caching of signature info +# For backward compatibility, the built-in scales will keep the *axis* parameter +# in their constructors until matplotlib 3.15, i.e. as long as the *axis* parameter +# is still supported. +_scale_has_axis_parameter = { + 'linear': True, + 'log': True, + 'symlog': True, + 'asinh': True, + 'logit': True, + 'function': True, + 'functionlog': True, +} + def get_scale_names(): """Return the names of the available scales.""" @@ -817,7 +839,11 @@ def scale_factory(scale, axis, **kwargs): axis : `~matplotlib.axis.Axis` """ scale_cls = _api.check_getitem(_scale_mapping, scale=scale) - return scale_cls(axis, **kwargs) + + if _scale_has_axis_parameter[scale]: + return scale_cls(axis, **kwargs) + else: + return scale_cls(**kwargs) if scale_factory.__doc__: @@ -836,6 +862,20 @@ def register_scale(scale_class): """ _scale_mapping[scale_class.name] = scale_class + # migration code to handle the *axis* parameter + has_axis_parameter = "axis" in inspect.signature(scale_class).parameters + _scale_has_axis_parameter[scale_class.name] = has_axis_parameter + if has_axis_parameter: + _api.warn_deprecated( + "3.11", + message=f"The scale {scale_class.__qualname__!r} uses an 'axis' parameter " + "in the constructors. This parameter is pending-deprecated since " + "matplotlib 3.11. It will be fully deprecated in 3.13 and removed " + "in 3.15. Starting with 3.11, 'register_scale()' accepts scales " + "without the *axis* parameter.", + pending=True, + ) + def _get_scale_docs(): """ diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index b3da951cf464..f98e083d84a0 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -6,8 +6,12 @@ LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale -from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation +from matplotlib.ticker import ( + AsinhLocator, AutoLocator, LogFormatterSciNotation, + NullFormatter, NullLocator, ScalarFormatter +) from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.transforms import IdentityTransform import numpy as np from numpy.testing import assert_allclose @@ -295,3 +299,75 @@ def test_bad_scale(self): AsinhScale(axis=None, linear_width=-1) s0 = AsinhScale(axis=None, ) s1 = AsinhScale(axis=None, linear_width=3.0) + + +def test_custom_scale_without_axis(): + """ + Test that one can register and use custom scales that don't take an *axis* param. + """ + class CustomTransform(IdentityTransform): + pass + + class CustomScale(mscale.ScaleBase): + name = "custom" + + # Important: __init__ has no *axis* parameter + def __init__(self): + self._transform = CustomTransform() + + def get_transform(self): + return self._transform + + def set_default_locators_and_formatters(self, axis): + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_locator(NullLocator()) + axis.set_minor_formatter(NullFormatter()) + + try: + mscale.register_scale(CustomScale) + fig, ax = plt.subplots() + ax.set_xscale('custom') + assert isinstance(ax.xaxis.get_transform(), CustomTransform) + finally: + # cleanup - there's no public unregister_scale() + del mscale._scale_mapping["custom"] + del mscale._scale_has_axis_parameter["custom"] + + +def test_custom_scale_with_axis(): + """ + Test that one can still register and use custom scales with an *axis* + parameter, but that registering issues a pending-deprecation warning. + """ + class CustomTransform(IdentityTransform): + pass + + class CustomScale(mscale.ScaleBase): + name = "custom" + + # Important: __init__ still has the *axis* parameter + def __init__(self, axis): + self._transform = CustomTransform() + + def get_transform(self): + return self._transform + + def set_default_locators_and_formatters(self, axis): + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_locator(NullLocator()) + axis.set_minor_formatter(NullFormatter()) + + try: + with pytest.warns( + PendingDeprecationWarning, + match=r"'axis' parameter .* is pending-deprecated"): + mscale.register_scale(CustomScale) + fig, ax = plt.subplots() + ax.set_xscale('custom') + assert isinstance(ax.xaxis.get_transform(), CustomTransform) + finally: + # cleanup - there's no public unregister_scale() + del mscale._scale_mapping["custom"] + del mscale._scale_has_axis_parameter["custom"] From e7a3852e8732a50245585454e2994361952dee38 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:17:36 +0200 Subject: [PATCH 2/2] Update doc/api/next_api_changes/deprecations/29358-TH.rst Co-authored-by: Elliott Sales de Andrade --- doc/api/next_api_changes/deprecations/29358-TH.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/next_api_changes/deprecations/29358-TH.rst b/doc/api/next_api_changes/deprecations/29358-TH.rst index baa0435a7bf1..1b7a50456afc 100644 --- a/doc/api/next_api_changes/deprecations/29358-TH.rst +++ b/doc/api/next_api_changes/deprecations/29358-TH.rst @@ -3,7 +3,7 @@ Since matplotlib 3.1 `PR 12831 `_ scale objects should be reusable and therefore independent of any particular Axis. -Therefore, the use of of the *axis* parameter in the ``__init__`` had been discouraged. +Therefore, the use of the *axis* parameter in the ``__init__`` had been discouraged. However, having that parameter in the signature was still necessary for API backwards-compatibility. This is no longer the case.