Skip to content

Commit e78a415

Browse files
committed
BUG: fix stacklevel munging for deprecation warnings of .resample()
Added a utility class (a context manager), check_deprecated_resample_kwargs, that sets the correct stacklevel and ensures that within the with statement, the warnings are emitted at most once. See the docstring for more info.
1 parent 27b16cf commit e78a415

File tree

7 files changed

+320
-85
lines changed

7 files changed

+320
-85
lines changed

pandas/core/frame.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -9140,20 +9140,23 @@ def resample(
91409140
origin: Union[str, "TimestampConvertibleTypes"] = "start_day",
91419141
offset: Optional["TimedeltaConvertibleTypes"] = None,
91429142
) -> "Resampler":
9143-
return super().resample(
9144-
rule=rule,
9145-
axis=axis,
9146-
closed=closed,
9147-
label=label,
9148-
convention=convention,
9149-
kind=kind,
9150-
loffset=loffset,
9151-
base=base,
9152-
on=on,
9153-
level=level,
9154-
origin=origin,
9155-
offset=offset,
9156-
)
9143+
from pandas.core.resample import check_deprecated_resample_kwargs
9144+
9145+
with check_deprecated_resample_kwargs(dict(base=base, loffset=loffset)):
9146+
return super().resample(
9147+
rule=rule,
9148+
axis=axis,
9149+
closed=closed,
9150+
label=label,
9151+
convention=convention,
9152+
kind=kind,
9153+
loffset=loffset,
9154+
base=base,
9155+
on=on,
9156+
level=level,
9157+
origin=origin,
9158+
offset=offset,
9159+
)
91579160

91589161
def to_timestamp(
91599162
self, freq=None, how: str = "start", axis: Axis = 0, copy: bool = True

pandas/core/generic.py

+18-17
Original file line numberDiff line numberDiff line change
@@ -8171,24 +8171,25 @@ def resample(
81718171
2000-10-02 00:41:00 24
81728172
Freq: 17T, dtype: int64
81738173
"""
8174-
from pandas.core.resample import get_resampler
8174+
from pandas.core.resample import check_deprecated_resample_kwargs, get_resampler
81758175

8176-
axis = self._get_axis_number(axis)
8177-
return get_resampler(
8178-
self,
8179-
freq=rule,
8180-
label=label,
8181-
closed=closed,
8182-
axis=axis,
8183-
kind=kind,
8184-
loffset=loffset,
8185-
convention=convention,
8186-
base=base,
8187-
key=on,
8188-
level=level,
8189-
origin=origin,
8190-
offset=offset,
8191-
)
8176+
with check_deprecated_resample_kwargs(dict(base=base, loffset=loffset)):
8177+
axis = self._get_axis_number(axis)
8178+
return get_resampler(
8179+
self,
8180+
freq=rule,
8181+
label=label,
8182+
closed=closed,
8183+
axis=axis,
8184+
kind=kind,
8185+
loffset=loffset,
8186+
convention=convention,
8187+
base=base,
8188+
key=on,
8189+
level=level,
8190+
origin=origin,
8191+
offset=offset,
8192+
)
81928193

81938194
def first(self: FrameOrSeries, offset) -> FrameOrSeries:
81948195
"""

pandas/core/groupby/groupby.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1796,9 +1796,13 @@ def resample(self, rule, *args, **kwargs):
17961796
2000-01-01 00:03:00 0 2
17971797
5 2000-01-01 00:03:00 5 1
17981798
"""
1799-
from pandas.core.resample import get_resampler_for_grouping
1799+
from pandas.core.resample import (
1800+
check_deprecated_resample_kwargs,
1801+
get_resampler_for_grouping,
1802+
)
18001803

1801-
return get_resampler_for_grouping(self, rule, *args, **kwargs)
1804+
with check_deprecated_resample_kwargs(kwargs):
1805+
return get_resampler_for_grouping(self, rule, *args, **kwargs)
18021806

18031807
@Substitution(name="groupby")
18041808
@Appender(_common_see_also)

pandas/core/groupby/grouper.py

+1-37
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
split-apply-combine paradigm.
44
"""
55
from typing import Dict, Hashable, List, Optional, Tuple
6-
import warnings
76

87
import numpy as np
98

@@ -228,43 +227,8 @@ def __new__(cls, *args, **kwargs):
228227
if kwargs.get("freq") is not None:
229228
from pandas.core.resample import TimeGrouper
230229

231-
# Deprecation warning of `base` and `loffset` since v1.1.0:
232-
# we are raising the warning here to be able to set the `stacklevel`
233-
# properly since we need to raise the `base` and `loffset` deprecation
234-
# warning from three different cases:
235-
# core/generic.py::NDFrame.resample
236-
# core/groupby/groupby.py::GroupBy.resample
237-
# core/groupby/grouper.py::Grouper
238-
# raising these warnings from TimeGrouper directly would fail the test:
239-
# tests/resample/test_deprecated.py::test_deprecating_on_loffset_and_base
240-
# hacky way to set the stacklevel: if cls is TimeGrouper it means
241-
# that the call comes from a pandas internal call of resample,
242-
# otherwise it comes from pd.Grouper
243-
stacklevel = 4 if cls is TimeGrouper else 2
244-
if kwargs.get("base", None) is not None:
245-
warnings.warn(
246-
"'base' in .resample() and in Grouper() is deprecated.\n"
247-
"The new arguments that you should use are 'offset' or 'origin'.\n"
248-
'\n>>> df.resample(freq="3s", base=2)\n'
249-
"\nbecomes:\n"
250-
'\n>>> df.resample(freq="3s", offset="2s")\n',
251-
FutureWarning,
252-
stacklevel=stacklevel,
253-
)
254-
255-
if kwargs.get("loffset", None) is not None:
256-
warnings.warn(
257-
"'loffset' in .resample() and in Grouper() is deprecated.\n"
258-
'\n>>> df.resample(freq="3s", loffset="8H")\n'
259-
"\nbecomes:\n"
260-
"\n>>> from pandas.tseries.frequencies import to_offset"
261-
'\n>>> df = df.resample(freq="3s").mean()'
262-
'\n>>> df.index = df.index.to_timestamp() + to_offset("8H")\n',
263-
FutureWarning,
264-
stacklevel=stacklevel,
265-
)
266-
267230
cls = TimeGrouper
231+
268232
return super().__new__(cls)
269233

270234
def __init__(

pandas/core/resample.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import timedelta
33
from textwrap import dedent
44
from typing import Dict, Optional, Union, no_type_check
5+
import warnings
56

67
import numpy as np
78

@@ -17,6 +18,7 @@
1718
from pandas._typing import TimedeltaConvertibleTypes, TimestampConvertibleTypes
1819
from pandas.compat.numpy import function as nv
1920
from pandas.errors import AbstractMethodError
21+
from pandas.util._call_once import call_once
2022
from pandas.util._decorators import Appender, Substitution, doc
2123

2224
from pandas.core.dtypes.generic import ABCDataFrame, ABCSeries
@@ -1333,6 +1335,9 @@ def __init__(
13331335
offset: Optional[TimedeltaConvertibleTypes] = None,
13341336
**kwargs,
13351337
):
1338+
with check_deprecated_resample_kwargs(dict(base=base, loffset=loffset)):
1339+
pass
1340+
13361341
# Check for correctness of the keyword arguments which would
13371342
# otherwise silently use the default if misspelled
13381343
if label not in {None, "left", "right"}:
@@ -1657,7 +1662,6 @@ def _get_period_bins(self, ax: PeriodIndex):
16571662

16581663

16591664
def _take_new_index(obj, indexer, new_index, axis=0):
1660-
16611665
if isinstance(obj, ABCSeries):
16621666
new_values = algos.take_1d(obj._values, indexer)
16631667
return obj._constructor(new_values, index=new_index, name=obj.name)
@@ -1913,3 +1917,54 @@ def _asfreq_compat(index, freq):
19131917
else:
19141918
new_index = Index([], dtype=index.dtype, freq=freq, name=index.name)
19151919
return new_index
1920+
1921+
1922+
def check_deprecated_resample_kwargs(kwargs, stacklevel=2):
1923+
"""
1924+
Utility function to emit the deprecation warnings of `base` and `loffset` (v1.1.0).
1925+
1926+
Its purpose is to allow passing `test_deprecating_on_loffset_and_base` without any
1927+
hacky manipulation of ``stacklevel`` (see L231-L266 of pandas.core.groupby.grouper
1928+
at commit 4a267c69).
1929+
1930+
Uses a :class:`pandas.util._call_once.CallOnceContextManager`. This idempotent
1931+
context manager ensures that only the outermost instance (which has the correct
1932+
value for stacklevel) is the one to emit the warnings when it is entered.
1933+
1934+
Parameters
1935+
----------
1936+
kwargs
1937+
Kewyword arguments dict to check.
1938+
stacklevel
1939+
The ``stacklevel`` of warnings from the perspective of the caller.
1940+
1941+
Returns
1942+
-------
1943+
A :class:`pandas.util._call_once.CallOnceContextManager` instance.
1944+
"""
1945+
stacklevel += 2 # one for __enter__() and one for callback()
1946+
1947+
def callback():
1948+
if kwargs.get("base", None) is not None:
1949+
warnings.warn(
1950+
"'base' in .resample() and in Grouper() is deprecated.\n"
1951+
"The new arguments that you should use are 'offset' or 'origin'.\n"
1952+
'\n>>> df.resample(freq="3s", base=2)\n'
1953+
"\nbecomes:\n"
1954+
'\n>>> df.resample(freq="3s", offset="2s")\n',
1955+
FutureWarning,
1956+
stacklevel=stacklevel,
1957+
)
1958+
if kwargs.get("loffset", None) is not None:
1959+
warnings.warn(
1960+
"'loffset' in .resample() and in Grouper() is deprecated.\n"
1961+
'\n>>> df.resample(freq="3s", loffset="8H")\n'
1962+
"\nbecomes:\n"
1963+
"\n>>> from pandas.tseries.frequencies import to_offset"
1964+
'\n>>> df = df.resample(freq="3s").mean()'
1965+
'\n>>> df.index = df.index.to_timestamp() + to_offset("8H")\n',
1966+
FutureWarning,
1967+
stacklevel=stacklevel,
1968+
)
1969+
1970+
return call_once(callback, key="check_deprecated_resample_kwargs")

pandas/core/series.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -4957,20 +4957,23 @@ def resample(
49574957
origin: Union[str, "TimestampConvertibleTypes"] = "start_day",
49584958
offset: Optional["TimedeltaConvertibleTypes"] = None,
49594959
) -> "Resampler":
4960-
return super().resample(
4961-
rule=rule,
4962-
axis=axis,
4963-
closed=closed,
4964-
label=label,
4965-
convention=convention,
4966-
kind=kind,
4967-
loffset=loffset,
4968-
base=base,
4969-
on=on,
4970-
level=level,
4971-
origin=origin,
4972-
offset=offset,
4973-
)
4960+
from pandas.core.resample import check_deprecated_resample_kwargs
4961+
4962+
with check_deprecated_resample_kwargs(dict(base=base, loffset=loffset)):
4963+
return super().resample(
4964+
rule=rule,
4965+
axis=axis,
4966+
closed=closed,
4967+
label=label,
4968+
convention=convention,
4969+
kind=kind,
4970+
loffset=loffset,
4971+
base=base,
4972+
on=on,
4973+
level=level,
4974+
origin=origin,
4975+
offset=offset,
4976+
)
49744977

49754978
def to_timestamp(self, freq=None, how="start", copy=True) -> "Series":
49764979
"""

0 commit comments

Comments
 (0)