Skip to content
Open
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
106 changes: 106 additions & 0 deletions src/modelskill/_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Deprecation utilities for ModelSkill.

This module provides utilities for deprecating positional arguments in functions,
following scikit-learn's SLEP009 approach.
"""

from __future__ import annotations

import warnings
from functools import wraps
from inspect import Parameter, signature
from typing import Any, Callable, TypeVar, overload

from . import __version__

# Type variable for the decorated function
F = TypeVar("F", bound=Callable[..., Any])


@overload
def _deprecate_positional_args(func: F) -> F: ...


@overload
def _deprecate_positional_args(
func: None = None, *, version: str = ...
) -> Callable[[F], F]: ...


def _deprecate_positional_args(
func: F | None = None, *, version: str = "2.0"
) -> F | Callable[[F], F]:
"""Decorator for methods that issues warnings for positional arguments.

Using the keyword-only argument syntax in PEP 3102, arguments after the
* will issue a warning when passed as a positional argument.

This is a temporary migration tool that will be removed in a future major version.
The decorator preserves type annotations and works with mypy.

Parameters
----------
func : callable, optional
Function to check arguments on.
version : str, default="2.0"
The version when positional arguments will result in an error.

Returns
-------
callable
Decorated function that warns when keyword-only args are passed positionally.

Examples
--------
>>> @_deprecate_positional_args
... def function(data, option1=None, option2=None):
... pass

>>> @_deprecate_positional_args(version="2.0")
... def function(data, option1=None, option2=None):
... pass
"""

def _inner_deprecate_positional_args(f: F) -> F:
sig = signature(f)
kwonly_args = []
all_args = []

for name, param in sig.parameters.items():
if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
all_args.append(name)
elif param.kind == Parameter.KEYWORD_ONLY:
kwonly_args.append(name)

@wraps(f)
def inner_f(*args: Any, **kwargs: Any) -> Any:
extra_args = len(args) - len(all_args)
if extra_args <= 0:
return f(*args, **kwargs)

# extra_args > 0
args_msg = [
f"{name}={arg}"
for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:])
]
args_msg_str = ", ".join(args_msg)

# Get current version without dev suffix
current_version = __version__.split(".dev")[0]

warnings.warn(
f"Passing {args_msg_str} as positional argument(s) is deprecated "
f"since version {current_version} and will raise an error in version {version}. "
f"Please use keyword argument(s) instead.",
FutureWarning,
stacklevel=2,
)
kwargs.update(zip(sig.parameters, args))
return f(**kwargs)

return inner_f # type: ignore[return-value]

if func is not None:
return _inner_deprecate_positional_args(func)

return _inner_deprecate_positional_args
3 changes: 3 additions & 0 deletions src/modelskill/comparison/_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from ..utils import _get_name
from ._comparison import Comparer
from ..metrics import _parse_metric
from .._deprecation import _deprecate_positional_args
from ._utils import (
_add_spatial_grid_to_df,
_groupby_df,
Expand Down Expand Up @@ -284,6 +285,7 @@ def merge(
cc = cc.merge(oc)
return cc

@_deprecate_positional_args
def sel(
self,
model: Optional[IdxOrNameTypes] = None,
Expand Down Expand Up @@ -566,6 +568,7 @@ def _add_as_col_if_not_in_index(
skilldf.insert(loc=0, column=field, value=unames[0])
return skilldf

@_deprecate_positional_args
def gridded_skill(
self,
bins: int = 5,
Expand Down
3 changes: 3 additions & 0 deletions src/modelskill/comparison/_collection_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ..settings import options
from ..utils import _get_idx
from ._comparer_plotter import quantiles_xy
from .._deprecation import _deprecate_positional_args


def _default_univarate_title(kind: str, cc: ComparerCollection) -> str:
Expand Down Expand Up @@ -780,6 +781,7 @@ def _residual_hist_one_model(

return ax

@_deprecate_positional_args
def spatial_overview(
self,
ax=None,
Expand Down Expand Up @@ -809,6 +811,7 @@ def spatial_overview(

return spatial_overview(obs, ax=ax, figsize=figsize, title=title)

@_deprecate_positional_args
def temporal_coverage(
self,
limit_to_model_period: bool = True,
Expand Down
3 changes: 3 additions & 0 deletions src/modelskill/comparison/_comparer_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .. import metrics as mtr
from ..utils import _get_idx
import matplotlib.colors as colors
from .._deprecation import _deprecate_positional_args
from ..plotting._misc import (
_get_fig_ax,
_xtick_directional,
Expand Down Expand Up @@ -259,6 +260,7 @@ def _hist_one_model(

return ax

@_deprecate_positional_args
def kde(self, ax=None, title=None, figsize=None, **kwargs) -> matplotlib.axes.Axes:
"""Plot kde (kernel density estimates of distributions) of model data and observations.

Expand Down Expand Up @@ -760,6 +762,7 @@ def taylor(
title=title,
)

@_deprecate_positional_args
def residual_hist(
self, bins=100, title=None, color=None, figsize=None, ax=None, **kwargs
) -> matplotlib.axes.Axes | list[matplotlib.axes.Axes]:
Expand Down
3 changes: 3 additions & 0 deletions src/modelskill/comparison/_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from ..timeseries._timeseries import _validate_data_var_name
from ._comparer_plotter import ComparerPlotter
from ..metrics import _parse_metric
from .._deprecation import _deprecate_positional_args

from ._utils import (
_add_spatial_grid_to_df,
Expand Down Expand Up @@ -772,6 +773,7 @@ def merge(
elif isinstance(other, ComparerCollection):
return ComparerCollection([self, *other])

@_deprecate_positional_args
def sel(
self,
model: Optional[IdxOrNameTypes] = None,
Expand Down Expand Up @@ -1045,6 +1047,7 @@ def score(
score = {str(k): float(v) for k, v in ser.items()}
return score

@_deprecate_positional_args
def gridded_skill(
self,
bins: int = 5,
Expand Down
4 changes: 4 additions & 0 deletions src/modelskill/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .plotting._misc import _get_fig_ax
from .metrics import small_is_best, large_is_best, zero_is_best, one_is_best
from ._deprecation import _deprecate_positional_args


# TODO remove ?
Expand Down Expand Up @@ -172,6 +173,7 @@ def barh(self, level: int | str = 0, **kwargs: Any) -> Axes:
self._name_to_title_in_kwargs(kwargs)
return df.plot.barh(**kwargs)

@_deprecate_positional_args
def grid(
self,
show_numbers: bool = True,
Expand Down Expand Up @@ -655,6 +657,7 @@ def query(self, query: str) -> SkillTable:
"""
return self.__class__(self.data.query(query))

@_deprecate_positional_args
def sel(
self, query: str | None = None, reduce_index: bool = True, **kwargs: Any
) -> SkillTable | SkillArray:
Expand Down Expand Up @@ -762,6 +765,7 @@ def round(self, decimals: int = 3) -> SkillTable:

return self.__class__(self.data.round(decimals=decimals))

@_deprecate_positional_args
def style(
self,
decimals: int = 3,
Expand Down