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..513628e0bd42 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29358-TH.rst @@ -0,0 +1,10 @@ +3rd party scales do not need to have an *axis* parameter anymore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since matplotlib 3.1 `PR 12831 `_ +scales should be reusable and therefore independent of the Axis. Therefore the use of +of the *axis* parameter in the ``__init__`` had been discouraged. However, that +parameter was still necessary for API compatibility. This is no longer the case. + +`.register_scale` now accepts scale classes with and without this parameter. +3rd party scales can and should remove that parameter. diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index ccaaae6caf5d..6f533f3fbd1a 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -690,6 +690,9 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'functionlog': FuncScaleLog, } +# caching of signature info +_scale_has_axis_parameter = {} + def get_scale_names(): """Return the names of the available scales.""" @@ -706,7 +709,19 @@ def scale_factory(scale, axis, **kwargs): axis : `~matplotlib.axis.Axis` """ scale_cls = _api.check_getitem(_scale_mapping, scale=scale) - return scale_cls(axis, **kwargs) + + # We support scales that may or may not have an initial *axis* parameter. + # This information is cached, so that we do not need to inspect the signature + # on every time we create a scale. + if scale not in _scale_has_axis_parameter: + _scale_has_axis_parameter[scale] = ( + "axis" in inspect.signature(scale_cls).parameters + ) + + if _scale_has_axis_parameter[scale]: + return scale_cls(axis, **kwargs) + else: + return scale_cls(**kwargs) if scale_factory.__doc__: diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 727397367762..945cbfc48f28 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 @@ -293,3 +297,35 @@ 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" + + 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"]