Skip to content

Commit 897017c

Browse files
committed
MNT: Registered 3rd party scales do not need an axis parameter anymore
First step of #29349.
1 parent 0a44bcb commit 897017c

File tree

3 files changed

+136
-6
lines changed

3 files changed

+136
-6
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
3rd party scales do not need to have an *axis* parameter anymore
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Since matplotlib 3.1 `PR 12831 <https://github.com/matplotlib/matplotlib/pull/12831>`_
5+
scales should be reusable and therefore independent of the Axis. Therefore, the use of
6+
of the *axis* parameter in the ``__init__`` had been discouraged. However, that
7+
parameter was still necessary for API compatibility. This is no longer the case.
8+
9+
`.register_scale` now accepts scale classes with or without this parameter.
10+
11+
The *axis* parameter is pending-deprecated. It will be deprecated in matplotlib 3.13,
12+
and removed in matplotlib 3.15.
13+
14+
3rd-party scales are recommended to remove the *axis* parameter now if they can
15+
afford to restrict compatibility to matplotlib >= 3.11 already. Otherwise, they may
16+
keep the *axis* parameter and remove it in time for matplotlib 3.13.

lib/matplotlib/scale.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,18 @@ def __init__(self, axis):
7777
For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
7878
object as the first argument.
7979
80-
The current recommendation for `.ScaleBase` subclasses is to have the
81-
*axis* parameter for API compatibility, but not make use of it. This is
82-
because we plan to remove this argument to make a scale object usable
83-
by multiple `~matplotlib.axis.Axis`\es at the same time.
80+
.. deprecated:: 3.11
81+
82+
The *axis* parameter is now optional, i.e. matplotlib is compatible
83+
with `.ScaleBase` subclasses that do not take an *axis* parameter.
84+
85+
The *axis* parameter is pending-deprecated. It will be deprecated
86+
in matplotlib 3.13, and removed in matplotlib 3.15.
87+
88+
3rd-party scales are recommended to remove the *axis* parameter now
89+
if they can afford to restrict compatibility to matplotlib >= 3.11
90+
already. Otherwise, they may keep the *axis* parameter and remove it
91+
in time for matplotlib 3.13.
8492
"""
8593

8694
def get_transform(self):
@@ -801,6 +809,20 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
801809
'functionlog': FuncScaleLog,
802810
}
803811

812+
# caching of signature info
813+
# For backward compatibility, the built-in scales will keep the *axis* parameter
814+
# in their constructors until matplotlib 3.15, i.e. as long as the *axis* parameter
815+
# is still supported.
816+
_scale_has_axis_parameter = {
817+
'linear': True,
818+
'log': True,
819+
'symlog': True,
820+
'asinh': True,
821+
'logit': True,
822+
'function': True,
823+
'functionlog': True,
824+
}
825+
804826

805827
def get_scale_names():
806828
"""Return the names of the available scales."""
@@ -817,7 +839,11 @@ def scale_factory(scale, axis, **kwargs):
817839
axis : `~matplotlib.axis.Axis`
818840
"""
819841
scale_cls = _api.check_getitem(_scale_mapping, scale=scale)
820-
return scale_cls(axis, **kwargs)
842+
843+
if _scale_has_axis_parameter[scale]:
844+
return scale_cls(axis, **kwargs)
845+
else:
846+
return scale_cls(**kwargs)
821847

822848

823849
if scale_factory.__doc__:
@@ -836,6 +862,20 @@ def register_scale(scale_class):
836862
"""
837863
_scale_mapping[scale_class.name] = scale_class
838864

865+
# migration code to handle the *axis* parameter
866+
has_axis_parameter = "axis" in inspect.signature(scale_class).parameters
867+
_scale_has_axis_parameter[scale_class.name] = has_axis_parameter
868+
if has_axis_parameter:
869+
_api.warn_deprecated(
870+
"3.11",
871+
message=f"The scale {scale_class.__qualname__!r} uses an 'axis' parameter "
872+
"in the constructors. This parameter is pending-deprecated since "
873+
"matplotlib 3.11. It will be fully deprecated in 3.13 and removed "
874+
"in 3.15. Starting with 3.11, 'register_scale()' accepts scales "
875+
"without the *axis* parameter.",
876+
pending=True,
877+
)
878+
839879

840880
def _get_scale_docs():
841881
"""

lib/matplotlib/tests/test_scale.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
LogTransform, InvertedLogTransform,
77
SymmetricalLogTransform)
88
import matplotlib.scale as mscale
9-
from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation
9+
from matplotlib.ticker import (
10+
AsinhLocator, AutoLocator, LogFormatterSciNotation,
11+
NullFormatter, NullLocator, ScalarFormatter
12+
)
1013
from matplotlib.testing.decorators import check_figures_equal, image_comparison
14+
from matplotlib.transforms import IdentityTransform
1115

1216
import numpy as np
1317
from numpy.testing import assert_allclose
@@ -295,3 +299,73 @@ def test_bad_scale(self):
295299
AsinhScale(axis=None, linear_width=-1)
296300
s0 = AsinhScale(axis=None, )
297301
s1 = AsinhScale(axis=None, linear_width=3.0)
302+
303+
304+
def test_custom_scale_without_axis():
305+
"""
306+
Test that one can register and use custom scales that don't take an *axis* param.
307+
"""
308+
class CustomTransform(IdentityTransform):
309+
pass
310+
311+
class CustomScale(mscale.ScaleBase):
312+
name = "custom"
313+
314+
def __init__(self):
315+
self._transform = CustomTransform()
316+
317+
def get_transform(self):
318+
return self._transform
319+
320+
def set_default_locators_and_formatters(self, axis):
321+
axis.set_major_locator(AutoLocator())
322+
axis.set_major_formatter(ScalarFormatter())
323+
axis.set_minor_locator(NullLocator())
324+
axis.set_minor_formatter(NullFormatter())
325+
326+
try:
327+
mscale.register_scale(CustomScale)
328+
fig, ax = plt.subplots()
329+
ax.set_xscale('custom')
330+
assert isinstance(ax.xaxis.get_transform(), CustomTransform)
331+
finally:
332+
# cleanup - there's no public unregister_scale()
333+
del mscale._scale_mapping["custom"]
334+
del mscale._scale_has_axis_parameter["custom"]
335+
336+
337+
def test_custom_scale_with_axis():
338+
"""
339+
Test that one can still register and use custom scales with an *axis*
340+
parameter, but that registering issues a pending-deprecation warning.
341+
"""
342+
class CustomTransform(IdentityTransform):
343+
pass
344+
345+
class CustomScale(mscale.ScaleBase):
346+
name = "custom"
347+
348+
def __init__(self, axis):
349+
self._transform = CustomTransform()
350+
351+
def get_transform(self):
352+
return self._transform
353+
354+
def set_default_locators_and_formatters(self, axis):
355+
axis.set_major_locator(AutoLocator())
356+
axis.set_major_formatter(ScalarFormatter())
357+
axis.set_minor_locator(NullLocator())
358+
axis.set_minor_formatter(NullFormatter())
359+
360+
try:
361+
with pytest.warns(
362+
PendingDeprecationWarning,
363+
match=r"'axis' parameter .* is pending-deprecated"):
364+
mscale.register_scale(CustomScale)
365+
fig, ax = plt.subplots()
366+
ax.set_xscale('custom')
367+
assert isinstance(ax.xaxis.get_transform(), CustomTransform)
368+
finally:
369+
# cleanup - there's no public unregister_scale()
370+
del mscale._scale_mapping["custom"]
371+
del mscale._scale_has_axis_parameter["custom"]

0 commit comments

Comments
 (0)