diff --git a/py-polars/polars/expr/expr.py b/py-polars/polars/expr/expr.py index f19c7108d329..36821e6b4f79 100644 --- a/py-polars/polars/expr/expr.py +++ b/py-polars/polars/expr/expr.py @@ -47,7 +47,7 @@ parse_as_list_of_expressions, ) from polars.utils.convert import _timedelta_to_pl_duration -from polars.utils.decorators import deprecated_alias +from polars.utils.decorators import deprecated_alias, warn_closed_future_change from polars.utils.meta import threadpool_size from polars.utils.various import sphinx_accessor @@ -4776,6 +4776,7 @@ def interpolate(self, method: InterpolationMethod = "linear") -> Self: """ return self._from_pyexpr(self._pyexpr.interpolate(method)) + @warn_closed_future_change() def rolling_min( self, window_size: int | timedelta | str, @@ -4973,6 +4974,7 @@ def rolling_min( ) ) + @warn_closed_future_change() def rolling_max( self, window_size: int | timedelta | str, @@ -5195,6 +5197,7 @@ def rolling_max( ) ) + @warn_closed_future_change() def rolling_mean( self, window_size: int | timedelta | str, @@ -5417,6 +5420,7 @@ def rolling_mean( ) ) + @warn_closed_future_change() def rolling_sum( self, window_size: int | timedelta | str, @@ -5639,6 +5643,7 @@ def rolling_sum( ) ) + @warn_closed_future_change() def rolling_std( self, window_size: int | timedelta | str, @@ -5864,6 +5869,7 @@ def rolling_std( ) ) + @warn_closed_future_change() def rolling_var( self, window_size: int | timedelta | str, @@ -6095,6 +6101,7 @@ def rolling_var( ) ) + @warn_closed_future_change() def rolling_median( self, window_size: int | timedelta | str, @@ -6242,6 +6249,7 @@ def rolling_median( ) ) + @warn_closed_future_change() def rolling_quantile( self, quantile: float, diff --git a/py-polars/polars/utils/decorators.py b/py-polars/polars/utils/decorators.py index d891be109bdf..37107ed5c722 100644 --- a/py-polars/polars/utils/decorators.py +++ b/py-polars/polars/utils/decorators.py @@ -41,6 +41,38 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return deco +def warn_closed_future_change() -> Callable[[Callable[P, T]], Callable[P, T]]: + """ + Warn that user should pass in 'closed' as default value will change. + + Decorator for rolling function. Use as follows: + + @warn_closed_future_change() + def myfunc(): + ... + """ + + def deco(function: Callable[P, T]) -> Callable[P, T]: + @wraps(function) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + # we only warn if 'by' is passed in, otherwise 'closed' is not used + if (kwargs.get("by") is not None) and ("closed" not in kwargs): + warnings.warn( + message=( + "The default argument for closed, 'left', will be changed to 'right' in the future." + "Fix this warning by explicitly passing in a value for closed" + ), + category=FutureWarning, + stacklevel=find_stacklevel(), + ) + + return function(*args, **kwargs) + + return wrapper + + return deco + + def _rename_kwargs( func_name: str, kwargs: dict[str, object], diff --git a/py-polars/tests/unit/operations/test_rolling.py b/py-polars/tests/unit/operations/test_rolling.py index 7c1f4232e4be..3887af4dd573 100644 --- a/py-polars/tests/unit/operations/test_rolling.py +++ b/py-polars/tests/unit/operations/test_rolling.py @@ -208,7 +208,9 @@ def test_rolling_crossing_dst( datetime(2021, 11, 5), datetime(2021, 11, 10), "1d", time_zone="UTC", eager=True ).dt.replace_time_zone(time_zone) df = pl.DataFrame({"ts": ts, "value": [1, 2, 3, 4, 5, 6]}) - result = df.with_columns(getattr(pl.col("value"), rolling_fn)("1d", by="ts")) + result = df.with_columns( + getattr(pl.col("value"), rolling_fn)("1d", by="ts", closed="left") + ) expected = pl.DataFrame({"ts": ts, "value": expected_values}) assert_frame_equal(result, expected) diff --git a/py-polars/tests/unit/test_lazy.py b/py-polars/tests/unit/test_lazy.py index d654eb9e9a99..eb55594e1dc9 100644 --- a/py-polars/tests/unit/test_lazy.py +++ b/py-polars/tests/unit/test_lazy.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from datetime import date, datetime from functools import reduce from inspect import signature @@ -725,6 +726,27 @@ def test_rolling(fruits_cars: pl.DataFrame) -> None: assert cast(float, out_single_val_variance[0, "var"]) == 0.0 +def test_rolling_closed_decorator() -> None: + # no warning if we do not use by + with warnings.catch_warnings(): + warnings.simplefilter("error") + _ = pl.col("a").rolling_min(2) + + # if we pass in a by, but no closed, we expect a warning + with pytest.warns(FutureWarning): + _ = pl.col("a").rolling_min(2, by="b") + + # if we pass in a by and a closed, we expect no warning + with warnings.catch_warnings(): + warnings.simplefilter("error") + _ = pl.col("a").rolling_min(2, by="b", closed="left") + + # regardless of the value + with warnings.catch_warnings(): + warnings.simplefilter("error") + _ = pl.col("a").rolling_min(2, by="b", closed="right") + + def test_arr_namespace(fruits_cars: pl.DataFrame) -> None: ldf = fruits_cars.lazy() out = ldf.select(