🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/api/next_api_changes/deprecations/29358-TH.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
3rd party scales do not need to have an *axis* parameter anymore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Since matplotlib 3.1 `PR 12831 <https://github.com/matplotlib/matplotlib/pull/12831>`_
scale objects should be reusable and therefore independent of any particular Axis.
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.

`.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.
50 changes: 45 additions & 5 deletions lib/matplotlib/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the separate variable, or can we get away with changing _scale_mapping to dict[str, tuple[bool, ScaleBase]]?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could change _scale_mapping instead. I've considered that. In the end, I've chosen the separate variable, because this functionality is only needed during the deprecation period. It's simpler to delete the extra variable rather than rewrite the logic of _scale_mapping back to the previous behavior when the deprecation expires. But one can argument both ways. I don't have a strong opinion, but also don't think it really matters.

'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."""
Expand All @@ -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__:
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that a subclass would name the parameter something other than "axis", in which case the better check here would be to see if there is a single positional only argument instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory that would be possible. However, (i) it would be deeply confusing and I doubt a reaonsable subclass would do this; and (ii) it is possible that a subclass has defined two positional parameters SpecialScale(axis, foo), they might remove the axis and end up with SpecialScale(foo). We do not require and cannot enforce that subclasses do not have other positional parameters - therefore we cannot detect whether SpecialScale(foo) wants to interpret foo as axis or not. Therefore, I'd rather go by the name.

_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():
"""
Expand Down
78 changes: 77 additions & 1 deletion lib/matplotlib/tests/test_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Loading