From d8fb0831d1e1dafa7254f9fa613c6e014b5719e0 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 29 Apr 2025 16:36:58 -0300 Subject: [PATCH 01/11] Adding a decorator and Refactoring functions --- lib/matplotlib/scale.py | 65 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 44fbe5209c4d..610ac2222806 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -31,6 +31,8 @@ import inspect import textwrap +import warnings +from functools import wraps import numpy as np @@ -103,6 +105,40 @@ def limit_range_for_scale(self, vmin, vmax, minpos): return vmin, vmax +def handle_axis_parameter(init_func): + """ + Allow scale classes to work with or without the `axis` parameter. + + This decorator enables scale constructors to maintain backward + compatibility with older code that passes `axis`, while allowing + future implementations to omit it entirely. + + If the wrapped constructor defines `axis` as its first argument, + the parameter is preserved. Otherwise, it is safely removed from + positional or keyword arguments. + + Parameters + ---------- + init_func : callable + The original __init__ method of a scale class. + + Returns + ------- + callable + A wrapped version of `init_func` that handles the optional `axis`. + """ + @wraps(init_func) + def wrapper(self, *args, **kwargs): + sig = inspect.signature(init_func) + params = list(sig.parameters.values()) + if params and params[1].name == "axis": + return init_func(self, *args, **kwargs) + if args: + args = args[1:] + kwargs.pop("axis", None) + return init_func(self, *args, **kwargs) + return wrapper + class LinearScale(ScaleBase): """ The default linear scale. @@ -110,7 +146,8 @@ class LinearScale(ScaleBase): name = 'linear' - def __init__(self, axis): + @handle_axis_parameter + def __init__(self, axis=None): # This method is present only to prevent inheritance of the base class' # constructor docstring, which would otherwise end up interpolated into # the docstring of Axis.set_scale. @@ -180,6 +217,7 @@ class FuncScale(ScaleBase): name = 'function' + @handle_axis_parameter def __init__(self, axis, functions): """ Parameters @@ -279,7 +317,8 @@ class LogScale(ScaleBase): """ name = 'log' - def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"): + @handle_axis_parameter + def __init__(self, axis=None, *, base=10, subs=None, nonpositive="clip"): """ Parameters ---------- @@ -330,6 +369,7 @@ class FuncScaleLog(LogScale): name = 'functionlog' + @handle_axis_parameter def __init__(self, axis, functions, base=10): """ Parameters @@ -455,7 +495,8 @@ class SymmetricalLogScale(ScaleBase): """ name = 'symlog' - def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1): + @handle_axis_parameter + def __init__(self, axis=None, *, base=10, linthresh=2, subs=None, linscale=1): self._transform = SymmetricalLogTransform(base, linthresh, linscale) self.subs = subs @@ -547,6 +588,7 @@ class AsinhScale(ScaleBase): 1024: (256, 512) } + @handle_axis_parameter def __init__(self, axis, *, linear_width=1.0, base=10, subs='auto', **kwargs): """ @@ -645,7 +687,8 @@ class LogitScale(ScaleBase): """ name = 'logit' - def __init__(self, axis, nonpositive='mask', *, + @handle_axis_parameter + def __init__(self, axis=None, nonpositive='mask', *, one_half=r"\frac{1}{2}", use_overline=False): r""" Parameters @@ -725,7 +768,12 @@ def scale_factory(scale, axis, **kwargs): axis : `~matplotlib.axis.Axis` """ scale_cls = _api.check_getitem(_scale_mapping, scale=scale) - return scale_cls(axis, **kwargs) + try: + return scale_cls(axis, **kwargs) + except TypeError as e: + if 'unexpected keyword argument' in str(e) or 'positional argument' in str(e): + return scale_cls(**kwargs) + raise if scale_factory.__doc__: @@ -742,6 +790,13 @@ def register_scale(scale_class): scale_class : subclass of `ScaleBase` The scale to register. """ + sig = inspect.signature(scale_class.__init__) + if 'axis' in sig.parameters: + warnings.warn( + f"The scale class {scale_class.__name__} still uses the 'axis' parameter in its constructor. " + "Consider refactoring it to remove this dependency.", + DeprecationWarning + ) _scale_mapping[scale_class.name] = scale_class From 1eea948629f5b4c1263b041cc980949bb9196e4f Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 13 May 2025 15:16:10 -0300 Subject: [PATCH 02/11] Fixing Ruff Errors --- lib/matplotlib/scale.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 610ac2222806..5ff6a8144081 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -139,6 +139,7 @@ def wrapper(self, *args, **kwargs): return init_func(self, *args, **kwargs) return wrapper + class LinearScale(ScaleBase): """ The default linear scale. @@ -317,7 +318,7 @@ class LogScale(ScaleBase): """ name = 'log' - @handle_axis_parameter + @handle_axis_parameter def __init__(self, axis=None, *, base=10, subs=None, nonpositive="clip"): """ Parameters @@ -793,8 +794,9 @@ def register_scale(scale_class): sig = inspect.signature(scale_class.__init__) if 'axis' in sig.parameters: warnings.warn( - f"The scale class {scale_class.__name__} still uses the 'axis' parameter in its constructor. " - "Consider refactoring it to remove this dependency.", + f"The scale class {scale_class.__name__} still uses the 'axis' " + "parameter in its constructor. Consider refactoring it to remove " + "this dependency.", DeprecationWarning ) _scale_mapping[scale_class.name] = scale_class From 93bf34b48b52710102a183907a6a40e13d0795a4 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 13 May 2025 15:26:12 -0300 Subject: [PATCH 03/11] Update scale.pyi --- lib/matplotlib/scale.pyi | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 7fec8e68cc5a..90a48fe7fd7a 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -2,7 +2,7 @@ from matplotlib.axis import Axis from matplotlib.transforms import Transform from collections.abc import Callable, Iterable -from typing import Literal +from typing import Literal, Union from numpy.typing import ArrayLike class ScaleBase: @@ -15,6 +15,7 @@ class ScaleBase: class LinearScale(ScaleBase): name: str + def __init__(self: ScaleBase, axis: Union[Axis, None] = None) -> None: ... class FuncTransform(Transform): input_dims: int @@ -56,12 +57,12 @@ class LogScale(ScaleBase): name: str subs: Iterable[int] | None def __init__( - self, - axis: Axis | None, + self: LogScale, + axis: Union[Axis, None] = None, *, - base: float = ..., - subs: Iterable[int] | None = ..., - nonpositive: Literal["clip", "mask"] = ... + base: float = 10, + subs: Union[Iterable[int], None] = None, + nonpositive: Union[Literal['clip'], Literal['mask']] = 'clip' ) -> None: ... @property def base(self) -> float: ... @@ -103,13 +104,13 @@ class SymmetricalLogScale(ScaleBase): name: str subs: Iterable[int] | None def __init__( - self, - axis: Axis | None, + self: SymmetricalLogScale, + axis: Union[Axis, None] = None, *, - base: float = ..., - linthresh: float = ..., - subs: Iterable[int] | None = ..., - linscale: float = ... + base: float = 10, + linthresh: float = 2, + subs: Union[Iterable[int], None] = None, + linscale: float = 1 ) -> None: ... @property def base(self) -> float: ... @@ -164,15 +165,16 @@ class LogisticTransform(Transform): class LogitScale(ScaleBase): name: str def __init__( - self, - axis: Axis | None, - nonpositive: Literal["mask", "clip"] = ..., + self: LogitScale, + axis: Union[Axis, None] = None, + nonpositive: Union[Literal['mask'], Literal['clip']] = 'mask', *, - one_half: str = ..., - use_overline: bool = ... + one_half: str = '\\frac{1}{2}', + use_overline: bool = False ) -> None: ... def get_transform(self) -> LogitTransform: ... def get_scale_names() -> list[str]: ... def scale_factory(scale: str, axis: Axis, **kwargs) -> ScaleBase: ... def register_scale(scale_class: type[ScaleBase]) -> None: ... +def handle_axis_parameter(init_func: Callable[..., None]) -> Callable[..., None]: ... \ No newline at end of file From d24176131deff8f5178b9924fd733c43ef54df75 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 13 May 2025 15:28:44 -0300 Subject: [PATCH 04/11] Adding new line to the end of scale.pyi --- lib/matplotlib/scale.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 90a48fe7fd7a..7e13b1c76ce8 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -177,4 +177,4 @@ class LogitScale(ScaleBase): def get_scale_names() -> list[str]: ... def scale_factory(scale: str, axis: Axis, **kwargs) -> ScaleBase: ... def register_scale(scale_class: type[ScaleBase]) -> None: ... -def handle_axis_parameter(init_func: Callable[..., None]) -> Callable[..., None]: ... \ No newline at end of file +def handle_axis_parameter(init_func: Callable[..., None]) -> Callable[..., None]: ... From 075c4b3d2bdb3248ab1841f46ee5ddd8550e07bc Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 13 May 2025 16:41:59 -0300 Subject: [PATCH 05/11] Update in docstring --- lib/matplotlib/scale.py | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 5ff6a8144081..c5ba764f8c3f 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -107,37 +107,37 @@ def limit_range_for_scale(self, vmin, vmax, minpos): def handle_axis_parameter(init_func): """ - Allow scale classes to work with or without the `axis` parameter. + Decorator to support scale constructors that optionally accept an axis. - This decorator enables scale constructors to maintain backward - compatibility with older code that passes `axis`, while allowing - future implementations to omit it entirely. - - If the wrapped constructor defines `axis` as its first argument, - the parameter is preserved. Otherwise, it is safely removed from - positional or keyword arguments. + This decorator provides backward compatibility for scale classes that + used to require an *axis* parameter. It allows scale constructors to + function whether or not the *axis* argument is provided. Parameters ---------- init_func : callable - The original __init__ method of a scale class. + The original ``__init__`` method of a scale class. Returns ------- callable - A wrapped version of `init_func` that handles the optional `axis`. + A wrapped version of ``init_func`` that supports the optional *axis* + parameter. + + Notes + ----- + If the constructor defines *axis* explicitly as its first parameter, the + argument is preserved. Otherwise, it is removed from positional and keyword + arguments before calling the constructor. + + Examples + -------- + >>> from matplotlib.scale import ScaleBase + >>> class CustomScale(ScaleBase): + ... @handle_axis_parameter + ... def __init__(self, axis=None, custom_param=1): + ... self.custom_param = custom_param """ - @wraps(init_func) - def wrapper(self, *args, **kwargs): - sig = inspect.signature(init_func) - params = list(sig.parameters.values()) - if params and params[1].name == "axis": - return init_func(self, *args, **kwargs) - if args: - args = args[1:] - kwargs.pop("axis", None) - return init_func(self, *args, **kwargs) - return wrapper class LinearScale(ScaleBase): From 5bef6c2e1bdaeea23ccf46b7001d659bba6df651 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 13 May 2025 16:47:49 -0300 Subject: [PATCH 06/11] Fixing Handle Function --- lib/matplotlib/scale.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index c5ba764f8c3f..6cb13003ef0c 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -107,28 +107,27 @@ def limit_range_for_scale(self, vmin, vmax, minpos): def handle_axis_parameter(init_func): """ - Decorator to support scale constructors that optionally accept an axis. + Decorator to handle the optional *axis* parameter in scale constructors. - This decorator provides backward compatibility for scale classes that - used to require an *axis* parameter. It allows scale constructors to - function whether or not the *axis* argument is provided. + This decorator ensures backward compatibility for scale classes that + previously required an *axis* parameter. It allows constructors to work + seamlessly with or without the *axis* parameter. Parameters ---------- init_func : callable - The original ``__init__`` method of a scale class. + The original __init__ method of a scale class. Returns ------- callable - A wrapped version of ``init_func`` that supports the optional *axis* - parameter. + A wrapped version of *init_func* that handles the optional *axis*. Notes ----- - If the constructor defines *axis* explicitly as its first parameter, the - argument is preserved. Otherwise, it is removed from positional and keyword - arguments before calling the constructor. + If the wrapped constructor defines *axis* as its first argument, the + parameter is preserved. Otherwise, it is safely removed from positional + or keyword arguments. Examples -------- @@ -138,6 +137,17 @@ def handle_axis_parameter(init_func): ... def __init__(self, axis=None, custom_param=1): ... self.custom_param = custom_param """ + @wraps(init_func) + def wrapper(self, *args, **kwargs): + sig = inspect.signature(init_func) + params = list(sig.parameters.values()) + if params and params[1].name == "axis": + return init_func(self, *args, **kwargs) + if args: + args = args[1:] + kwargs.pop("axis", None) + return init_func(self, *args, **kwargs) + return wrapper class LinearScale(ScaleBase): From 5cceb7a7231db14fd56ba4fedde59ab4aee730f8 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 20 May 2025 16:16:51 -0300 Subject: [PATCH 07/11] Support optional axis in scales Updated my refactor based on the feedbacks received --- lib/matplotlib/scale.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 6cb13003ef0c..5c6c7c5e3863 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -126,8 +126,8 @@ def handle_axis_parameter(init_func): Notes ----- If the wrapped constructor defines *axis* as its first argument, the - parameter is preserved. Otherwise, it is safely removed from positional - or keyword arguments. + parameter is preserved when present. Otherwise, the value `None` is injected + as the first argument. Examples -------- @@ -139,14 +139,12 @@ def handle_axis_parameter(init_func): """ @wraps(init_func) def wrapper(self, *args, **kwargs): - sig = inspect.signature(init_func) - params = list(sig.parameters.values()) - if params and params[1].name == "axis": + # If the first argument is a ScaleBase (axis), pass as is. + if args and isinstance(args[0], ScaleBase): return init_func(self, *args, **kwargs) - if args: - args = args[1:] - kwargs.pop("axis", None) - return init_func(self, *args, **kwargs) + else: + # Inject None as axis parameter + return init_func(self, None, *args, **kwargs) return wrapper @@ -801,14 +799,6 @@ def register_scale(scale_class): scale_class : subclass of `ScaleBase` The scale to register. """ - sig = inspect.signature(scale_class.__init__) - if 'axis' in sig.parameters: - warnings.warn( - f"The scale class {scale_class.__name__} still uses the 'axis' " - "parameter in its constructor. Consider refactoring it to remove " - "this dependency.", - DeprecationWarning - ) _scale_mapping[scale_class.name] = scale_class From d18d7a17388d4e8eb286f21c459e62b266783b85 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 20 May 2025 16:21:12 -0300 Subject: [PATCH 08/11] Fixing ruff error --- lib/matplotlib/scale.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 5c6c7c5e3863..d093c518c547 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -31,7 +31,6 @@ import inspect import textwrap -import warnings from functools import wraps import numpy as np From 538dbd2adedc9717cde32f2cb7a275bd6f368ed2 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 20 May 2025 18:34:35 -0300 Subject: [PATCH 09/11] change in parameters and in decorator --- lib/matplotlib/scale.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d093c518c547..489a2b25aaf5 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -133,16 +133,16 @@ def handle_axis_parameter(init_func): >>> from matplotlib.scale import ScaleBase >>> class CustomScale(ScaleBase): ... @handle_axis_parameter - ... def __init__(self, axis=None, custom_param=1): + ... def __init__(self, axis, custom_param=1): ... self.custom_param = custom_param """ @wraps(init_func) def wrapper(self, *args, **kwargs): - # If the first argument is a ScaleBase (axis), pass as is. - if args and isinstance(args[0], ScaleBase): + if args and isinstance(args[0], mpl.axis.Axis): return init_func(self, *args, **kwargs) else: - # Inject None as axis parameter + # Remove 'axis' from kwargs to avoid double assignment + kwargs.pop('axis', None) return init_func(self, None, *args, **kwargs) return wrapper @@ -155,7 +155,7 @@ class LinearScale(ScaleBase): name = 'linear' @handle_axis_parameter - def __init__(self, axis=None): + def __init__(self, axis): # This method is present only to prevent inheritance of the base class' # constructor docstring, which would otherwise end up interpolated into # the docstring of Axis.set_scale. @@ -326,7 +326,7 @@ class LogScale(ScaleBase): name = 'log' @handle_axis_parameter - def __init__(self, axis=None, *, base=10, subs=None, nonpositive="clip"): + def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"): """ Parameters ---------- @@ -504,7 +504,7 @@ class SymmetricalLogScale(ScaleBase): name = 'symlog' @handle_axis_parameter - def __init__(self, axis=None, *, base=10, linthresh=2, subs=None, linscale=1): + def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1): self._transform = SymmetricalLogTransform(base, linthresh, linscale) self.subs = subs @@ -696,7 +696,7 @@ class LogitScale(ScaleBase): name = 'logit' @handle_axis_parameter - def __init__(self, axis=None, nonpositive='mask', *, + def __init__(self, axis, nonpositive='mask', *, one_half=r"\frac{1}{2}", use_overline=False): r""" Parameters From 851b25cb468dc970f777aeda7e3e1617a3d3ce14 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 20 May 2025 18:51:04 -0300 Subject: [PATCH 10/11] parameter fix --- lib/matplotlib/scale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 489a2b25aaf5..3126c2c82095 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -155,7 +155,7 @@ class LinearScale(ScaleBase): name = 'linear' @handle_axis_parameter - def __init__(self, axis): + def __init__(self, axis=None): # This method is present only to prevent inheritance of the base class' # constructor docstring, which would otherwise end up interpolated into # the docstring of Axis.set_scale. @@ -326,7 +326,7 @@ class LogScale(ScaleBase): name = 'log' @handle_axis_parameter - def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"): + def __init__(self, axis=None, *, base=10, subs=None, nonpositive="clip"): """ Parameters ---------- @@ -504,7 +504,7 @@ class SymmetricalLogScale(ScaleBase): name = 'symlog' @handle_axis_parameter - def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1): + def __init__(self, axis=None, *, base=10, linthresh=2, subs=None, linscale=1): self._transform = SymmetricalLogTransform(base, linthresh, linscale) self.subs = subs @@ -597,7 +597,7 @@ class AsinhScale(ScaleBase): } @handle_axis_parameter - def __init__(self, axis, *, linear_width=1.0, + def __init__(self, axis=None, *, linear_width=1.0, base=10, subs='auto', **kwargs): """ Parameters @@ -696,7 +696,7 @@ class LogitScale(ScaleBase): name = 'logit' @handle_axis_parameter - def __init__(self, axis, nonpositive='mask', *, + def __init__(self, axis=None, nonpositive='mask', *, one_half=r"\frac{1}{2}", use_overline=False): r""" Parameters From 1a516b10eaedb61bcd498ff7f7bb49bcd2812888 Mon Sep 17 00:00:00 2001 From: Vagner Messias Date: Tue, 20 May 2025 18:57:14 -0300 Subject: [PATCH 11/11] minor change in pyi --- lib/matplotlib/scale.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 7e13b1c76ce8..d7f34457ad4f 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -139,7 +139,7 @@ class AsinhScale(ScaleBase): auto_tick_multipliers: dict[int, tuple[int, ...]] def __init__( self, - axis: Axis | None, + axis: Union[Axis, None] = None, *, linear_width: float = ..., base: float = ...,