Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEPR: attrs #52152

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Deprecations
- Deprecated 'method', 'limit', and 'fill_axis' keywords in :meth:`DataFrame.align` and :meth:`Series.align`, explicitly call ``fillna`` on the alignment results instead (:issue:`51856`)
- Deprecated 'broadcast_axis' keyword in :meth:`Series.align` and :meth:`DataFrame.align`, upcast before calling ``align`` with ``left = DataFrame({col: left for col in right.columns}, index=right.index)`` (:issue:`51856`)
- Deprecated the 'axis' keyword in :meth:`.GroupBy.idxmax`, :meth:`.GroupBy.idxmin`, :meth:`.GroupBy.fillna`, :meth:`.GroupBy.take`, :meth:`.GroupBy.skew`, :meth:`.GroupBy.rank`, :meth:`.GroupBy.cumprod`, :meth:`.GroupBy.cumsum`, :meth:`.GroupBy.cummax`, :meth:`.GroupBy.cummin`, :meth:`.GroupBy.pct_change`, :meth:`GroupBy.diff`, :meth:`.GroupBy.shift`, and :meth:`DataFrameGroupBy.corrwith`; for ``axis=1`` operate on the underlying :class:`DataFrame` instead (:issue:`50405`, :issue:`51046`)
- Deprecated :meth:`DataFrame.attrs`, :meth:`Series.attrs`, to retain the old attribute propagation override ``__finalize__`` in a subclass (:issue:`51280`)
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure that creating a subclass is the advice we want to give to users to retain the behaviour (generally users should be very wary about creating a custom subclass).

Copy link
Member Author

Choose a reason for hiding this comment

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

That seems reasonable. Did you have something else in mind?

Copy link
Member

Choose a reason for hiding this comment

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

Did you have something else in mind?

No, I am not aware of a good alternative. But so that's the part we should discuss before deprecating it.

-

.. ---------------------------------------------------------------------------
Expand Down
25 changes: 19 additions & 6 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,25 @@ def attrs(self) -> dict[Hashable, Any]:
--------
DataFrame.flags : Global flags applying to this object.
"""
warnings.warn(
f"{type(self).__name__}.attrs is deprecated and will be removed "
"in a future version",
FutureWarning,
stacklevel=find_stack_level(),
)

if self._attrs is None:
self._attrs = {}
return self._attrs

@attrs.setter
def attrs(self, value: Mapping[Hashable, Any]) -> None:
warnings.warn(
f"{type(self).__name__}.attrs is deprecated and will be removed "
"in a future version",
FutureWarning,
stacklevel=find_stack_level(),
)
self._attrs = dict(value)

@final
Expand Down Expand Up @@ -2018,7 +2031,7 @@ def __getstate__(self) -> dict[str, Any]:
"_mgr": self._mgr,
"_typ": self._typ,
"_metadata": self._metadata,
"attrs": self.attrs,
"_attrs": self._attrs,
"_flags": {k: self.flags[k] for k in self.flags._keys},
**meta,
}
Expand Down Expand Up @@ -6000,8 +6013,8 @@ def __finalize__(self, other, method: str | None = None, **kwargs) -> Self:
stable across pandas releases.
"""
if isinstance(other, NDFrame):
for name in other.attrs:
self.attrs[name] = other.attrs[name]
for name in other._attrs:
self._attrs[name] = other._attrs[name]

self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
# For subclasses using _metadata.
Expand All @@ -6010,11 +6023,11 @@ def __finalize__(self, other, method: str | None = None, **kwargs) -> Self:
object.__setattr__(self, name, getattr(other, name, None))

if method == "concat":
attrs = other.objs[0].attrs
check_attrs = all(objs.attrs == attrs for objs in other.objs[1:])
attrs = other.objs[0]._attrs
check_attrs = all(objs._attrs == attrs for objs in other.objs[1:])
if check_attrs:
for name in attrs:
self.attrs[name] = attrs[name]
self._attrs[name] = attrs[name]

allows_duplicate_labels = all(
x.flags.allows_duplicate_labels for x in other.objs
Expand Down
9 changes: 6 additions & 3 deletions pandas/tests/frame/methods/test_astype.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,10 +801,13 @@ def test_astype_noncontiguous(self, index_slice):
def test_astype_retain_attrs(self, any_numpy_dtype):
# GH#44414
df = DataFrame({"a": [0, 1, 2], "b": [3, 4, 5]})
df.attrs["Location"] = "Michigan"
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
df.attrs["Location"] = "Michigan"

result = df.astype({"a": any_numpy_dtype}).attrs
expected = df.attrs
with tm.assert_produces_warning(FutureWarning, match=msg):
result = df.astype({"a": any_numpy_dtype}).attrs
expected = df.attrs

tm.assert_dict_equal(expected, result)

Expand Down
9 changes: 6 additions & 3 deletions pandas/tests/frame/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,14 @@ async def test_tab_complete_warning(self, ip, frame_or_series):

def test_attrs(self):
df = DataFrame({"A": [2, 3]})
assert df.attrs == {}
df.attrs["version"] = 1
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
assert df.attrs == {}
df.attrs["version"] = 1

result = df.rename(columns=str)
assert result.attrs == {"version": 1}
with tm.assert_produces_warning(FutureWarning, match=msg):
assert result.attrs == {"version": 1}

@pytest.mark.parametrize("allows_duplicate_labels", [True, False, None])
def test_set_flags(
Expand Down
90 changes: 65 additions & 25 deletions pandas/tests/generic/test_finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest

import pandas as pd
import pandas._testing as tm

# TODO:
# * Binary methods (mul, div, etc.)
Expand Down Expand Up @@ -457,19 +458,26 @@ def test_finalize_called(ndframe_method):
cls, init_args, method = ndframe_method
ndframe = cls(*init_args)

ndframe.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
ndframe.attrs = {"a": 1}
result = method(ndframe)

assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@not_implemented_mark
def test_finalize_called_eval_numexpr():
pytest.importorskip("numexpr")
warn_msg = "(DataFrame|Series).attrs is deprecated"

df = pd.DataFrame({"A": [1, 2]})
df.attrs["A"] = 1
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
df.attrs["A"] = 1
result = df.eval("A + 1", engine="numexpr")
assert result.attrs == {"A": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"A": 1}


# ----------------------------------------------------------------------------
Expand All @@ -491,13 +499,18 @@ def test_finalize_called_eval_numexpr():
],
ids=lambda x: f"({type(x[0]).__name__},{type(x[1]).__name__})",
)
@pytest.mark.filterwarnings("ignore:Series.attrs is deprecated:FutureWarning")
def test_binops(request, args, annotate, all_binary_operators):
# This generates 624 tests... Is that needed?
left, right = args

warn_msg = "(DataFrame|Series).attrs is deprecated"
if isinstance(left, (pd.DataFrame, pd.Series)):
left.attrs = {}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
Copy link
Member

Choose a reason for hiding this comment

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

can probably remove this if you're already filtering the warnings from the test?

left.attrs = {}
if isinstance(right, (pd.DataFrame, pd.Series)):
right.attrs = {}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
right.attrs = {}

if annotate == "left" and isinstance(left, int):
pytest.skip("left is an int and doesn't support .attrs")
Expand Down Expand Up @@ -552,9 +565,11 @@ def test_binops(request, args, annotate, all_binary_operators):
)
)
if annotate in {"left", "both"} and not isinstance(left, int):
left.attrs = {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
left.attrs = {"a": 1}
if annotate in {"right", "both"} and not isinstance(right, int):
right.attrs = {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
right.attrs = {"a": 1}

is_cmp = all_binary_operators in [
operator.eq,
Expand All @@ -571,7 +586,8 @@ def test_binops(request, args, annotate, all_binary_operators):
right, left = right.align(left, axis=1, copy=False)

result = all_binary_operators(left, right)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


# ----------------------------------------------------------------------------
Expand Down Expand Up @@ -633,9 +649,12 @@ def test_binops(request, args, annotate, all_binary_operators):
)
def test_string_method(method):
s = pd.Series(["a1"])
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = method(s.str)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
Expand All @@ -655,9 +674,12 @@ def test_string_method(method):
)
def test_datetime_method(method):
s = pd.Series(pd.date_range("2000", periods=4))
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = method(s.dt)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
Expand Down Expand Up @@ -692,27 +714,36 @@ def test_datetime_method(method):
)
def test_datetime_property(attr):
s = pd.Series(pd.date_range("2000", periods=4))
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = getattr(s.dt, attr)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
"attr", ["days", "seconds", "microseconds", "nanoseconds", "components"]
)
def test_timedelta_property(attr):
s = pd.Series(pd.timedelta_range("2000", periods=4))
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = getattr(s.dt, attr)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize("method", [operator.methodcaller("total_seconds")])
def test_timedelta_methods(method):
s = pd.Series(pd.timedelta_range("2000", periods=4))
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = method(s.dt)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
Expand All @@ -732,9 +763,12 @@ def test_timedelta_methods(method):
@not_implemented_mark
def test_categorical_accessor(method):
s = pd.Series(["a", "b"], dtype="category")
s.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
s.attrs = {"a": 1}
result = method(s.cat)
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


# ----------------------------------------------------------------------------
Expand All @@ -755,9 +789,12 @@ def test_categorical_accessor(method):
],
)
def test_groupby_finalize(obj, method):
obj.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
obj.attrs = {"a": 1}
result = method(obj.groupby([0, 0], group_keys=False))
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


@pytest.mark.parametrize(
Expand All @@ -777,9 +814,12 @@ def test_groupby_finalize(obj, method):
)
@not_implemented_mark
def test_groupby_finalize_not_implemented(obj, method):
obj.attrs = {"a": 1}
warn_msg = "(DataFrame|Series).attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
obj.attrs = {"a": 1}
result = method(obj.groupby([0, 0]))
assert result.attrs == {"a": 1}
with tm.assert_produces_warning(FutureWarning, match=warn_msg):
assert result.attrs == {"a": 1}


def test_finalize_frame_series_name():
Expand Down
21 changes: 15 additions & 6 deletions pandas/tests/interchange/test_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def test_categorical_dtype(data):
desc_cat["categories"]._col, pd.Series(["a", "d", "e", "s", "t"])
)

tm.assert_frame_equal(df, from_dataframe(df.__dataframe__()))
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
res = from_dataframe(df.__dataframe__())
tm.assert_frame_equal(df, res)


@pytest.mark.parametrize(
Expand All @@ -90,12 +93,15 @@ def test_dataframe(data):
indices = (0, 2)
names = tuple(list(data.keys())[idx] for idx in indices)

result = from_dataframe(df2.select_columns(indices))
expected = from_dataframe(df2.select_columns_by_name(names))
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
result = from_dataframe(df2.select_columns(indices))
expected = from_dataframe(df2.select_columns_by_name(names))
tm.assert_frame_equal(result, expected)

assert isinstance(result.attrs["_INTERCHANGE_PROTOCOL_BUFFERS"], list)
assert isinstance(expected.attrs["_INTERCHANGE_PROTOCOL_BUFFERS"], list)
with tm.assert_produces_warning(FutureWarning, match=msg):
assert isinstance(result.attrs["_INTERCHANGE_PROTOCOL_BUFFERS"], list)
assert isinstance(expected.attrs["_INTERCHANGE_PROTOCOL_BUFFERS"], list)


def test_missing_from_masked():
Expand Down Expand Up @@ -193,7 +199,10 @@ def test_datetime():
assert col.dtype[0] == DtypeKind.DATETIME
assert col.describe_null == (ColumnNullType.USE_SENTINEL, iNaT)

tm.assert_frame_equal(df, from_dataframe(df.__dataframe__()))
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
res = from_dataframe(df.__dataframe__())
tm.assert_frame_equal(df, res)


@td.skip_if_np_lt("1.23")
Expand Down
6 changes: 4 additions & 2 deletions pandas/tests/io/test_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,10 @@ def test_pickle_frame_v124_unpickle_130(datapath):
"1.2.4",
"empty_frame_v1_2_4-GH#42345.pkl",
)
with open(path, "rb") as fd:
df = pickle.load(fd)
msg = "DataFrame.attrs is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
with open(path, "rb") as fd:
df = pickle.load(fd)

expected = pd.DataFrame(index=[], columns=[])
tm.assert_frame_equal(df, expected)
Loading