diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 644a7e86ec429..025ec1a79a95d 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -245,6 +245,7 @@ Deprecations - Deprecated :meth:`.Styler.applymap_index`. Use the new :meth:`.Styler.map_index` method instead (:issue:`52708`) - Deprecated :meth:`.Styler.applymap`. Use the new :meth:`.Styler.map` method instead (:issue:`52708`) - Deprecated :meth:`DataFrame.applymap`. Use the new :meth:`DataFrame.map` method instead (:issue:`52353`) +- Deprecated :meth:`DataFrame.attrs`, :meth:`Series.attrs`, to retain the old attribute propagation override ``__finalize__`` in a subclass (:issue:`51280`) - Deprecated :meth:`DataFrame.swapaxes` and :meth:`Series.swapaxes`, use :meth:`DataFrame.transpose` or :meth:`Series.transpose` instead (:issue:`51946`) - Deprecated ``freq`` parameter in :class:`PeriodArray` constructor, pass ``dtype`` instead (:issue:`52462`) - Deprecated behavior of :func:`concat` when :class:`DataFrame` has columns that are all-NA, in a future version these will not be discarded when determining the resulting dtype (:issue:`40893`) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index b6d862ba2180c..8f833c4e103b4 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -338,12 +338,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 @@ -2058,7 +2071,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, } @@ -6058,8 +6071,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. @@ -6068,11 +6081,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 diff --git a/pandas/tests/frame/methods/test_astype.py b/pandas/tests/frame/methods/test_astype.py index 08546f03cee69..96af7960c62d1 100644 --- a/pandas/tests/frame/methods/test_astype.py +++ b/pandas/tests/frame/methods/test_astype.py @@ -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) diff --git a/pandas/tests/frame/test_api.py b/pandas/tests/frame/test_api.py index e0d9d6c281fd5..f77ba4223d0f6 100644 --- a/pandas/tests/frame/test_api.py +++ b/pandas/tests/frame/test_api.py @@ -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( diff --git a/pandas/tests/generic/test_finalize.py b/pandas/tests/generic/test_finalize.py index d6c4dff055748..cf60d450b8825 100644 --- a/pandas/tests/generic/test_finalize.py +++ b/pandas/tests/generic/test_finalize.py @@ -8,6 +8,7 @@ import pytest import pandas as pd +import pandas._testing as tm # TODO: # * Binary methods (mul, div, etc.) @@ -446,19 +447,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} # ---------------------------------------------------------------------------- @@ -480,13 +488,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): + 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") @@ -541,9 +554,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, @@ -560,7 +575,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} # ---------------------------------------------------------------------------- @@ -622,9 +638,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( @@ -644,9 +663,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( @@ -681,9 +703,12 @@ 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( @@ -691,17 +716,23 @@ def test_datetime_property(attr): ) 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( @@ -721,9 +752,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} # ---------------------------------------------------------------------------- @@ -744,9 +778,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( @@ -766,9 +803,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(): diff --git a/pandas/tests/interchange/test_impl.py b/pandas/tests/interchange/test_impl.py index d393ba6fd3957..841a5fa72adc0 100644 --- a/pandas/tests/interchange/test_impl.py +++ b/pandas/tests/interchange/test_impl.py @@ -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) def test_categorical_pyarrow(): @@ -81,7 +84,9 @@ def test_categorical_pyarrow(): arr = ["Mon", "Tue", "Mon", "Wed", "Mon", "Thu", "Fri", "Sat", "Sun"] table = pa.table({"weekday": pa.array(arr).dictionary_encode()}) exchange_df = table.__dataframe__() - result = from_dataframe(exchange_df) + msg = "DataFrame.attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + result = from_dataframe(exchange_df) weekday = pd.Categorical( arr, categories=["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] ) @@ -96,7 +101,9 @@ def test_large_string_pyarrow(): arr = ["Mon", "Tue"] table = pa.table({"weekday": pa.array(arr, "large_string")}) exchange_df = table.__dataframe__() - result = from_dataframe(exchange_df) + msg = "DataFrame.attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + result = from_dataframe(exchange_df) expected = pd.DataFrame({"weekday": ["Mon", "Tue"]}) tm.assert_frame_equal(result, expected) @@ -122,7 +129,9 @@ def test_bitmasks_pyarrow(offset, length, expected_values): arr = [3.3, None, 2.1] table = pa.table({"arr": arr}).slice(offset, length) exchange_df = table.__dataframe__() - result = from_dataframe(exchange_df) + msg = "DataFrame.attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + result = from_dataframe(exchange_df) expected = pd.DataFrame({"arr": expected_values}) tm.assert_frame_equal(result, expected) @@ -146,12 +155,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(): @@ -249,7 +261,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") diff --git a/pandas/tests/io/test_pickle.py b/pandas/tests/io/test_pickle.py index 60506aa2fbd0a..a1a00cf4caa90 100644 --- a/pandas/tests/io/test_pickle.py +++ b/pandas/tests/io/test_pickle.py @@ -115,6 +115,9 @@ def test_flatten_buffer(data): assert result.shape == (result.nbytes,) +@pytest.mark.filterwarnings( + "ignore:(DataFrame|Series).attrs is deprecated:FutureWarning" +) def test_pickles(datapath): if not is_platform_little_endian(): pytest.skip("known failure on non-little endian") @@ -583,8 +586,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) diff --git a/pandas/tests/reshape/concat/test_concat.py b/pandas/tests/reshape/concat/test_concat.py index ef02e9f7a465a..4008d2c1768e9 100644 --- a/pandas/tests/reshape/concat/test_concat.py +++ b/pandas/tests/reshape/concat/test_concat.py @@ -714,11 +714,15 @@ def test_concat_multiindex_with_empty_rangeindex(): def test_concat_drop_attrs(data): # GH#41828 df1 = data.copy() - df1.attrs = {1: 1} + msg = "(Series|DataFrame).attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + df1.attrs = {1: 1} df2 = data.copy() - df2.attrs = {1: 2} + with tm.assert_produces_warning(FutureWarning, match=msg): + df2.attrs = {1: 2} df = concat([df1, df2]) - assert len(df.attrs) == 0 + with tm.assert_produces_warning(FutureWarning, match=msg): + assert len(df.attrs) == 0 @pytest.mark.parametrize( @@ -737,11 +741,15 @@ def test_concat_drop_attrs(data): def test_concat_retain_attrs(data): # GH#41828 df1 = data.copy() - df1.attrs = {1: 1} + msg = "(Series|DataFrame).attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + df1.attrs = {1: 1} df2 = data.copy() - df2.attrs = {1: 1} + with tm.assert_produces_warning(FutureWarning, match=msg): + df2.attrs = {1: 1} df = concat([df1, df2]) - assert df.attrs[1] == 1 + with tm.assert_produces_warning(FutureWarning, match=msg): + assert df.attrs[1] == 1 @td.skip_array_manager_invalid_test diff --git a/pandas/tests/series/methods/test_astype.py b/pandas/tests/series/methods/test_astype.py index 71ce8541de24b..29004bccedb78 100644 --- a/pandas/tests/series/methods/test_astype.py +++ b/pandas/tests/series/methods/test_astype.py @@ -441,10 +441,13 @@ def test_astype_ea_to_datetimetzdtype(self, dtype): def test_astype_retain_attrs(self, any_numpy_dtype): # GH#44414 ser = Series([0, 1, 2, 3]) - ser.attrs["Location"] = "Michigan" + msg = "Series.attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + ser.attrs["Location"] = "Michigan" - result = ser.astype(any_numpy_dtype).attrs - expected = ser.attrs + with tm.assert_produces_warning(FutureWarning, match=msg): + result = ser.astype(any_numpy_dtype).attrs + expected = ser.attrs tm.assert_dict_equal(expected, result) diff --git a/pandas/tests/series/test_api.py b/pandas/tests/series/test_api.py index e4e276af121f9..6100bb759d94f 100644 --- a/pandas/tests/series/test_api.py +++ b/pandas/tests/series/test_api.py @@ -161,10 +161,13 @@ def test_integer_series_size(self, dtype): def test_attrs(self): s = Series([0, 1], name="abc") - assert s.attrs == {} - s.attrs["version"] = 1 + msg = "Series.attrs is deprecated" + with tm.assert_produces_warning(FutureWarning, match=msg): + assert s.attrs == {} + s.attrs["version"] = 1 result = s + 1 - assert result.attrs == {"version": 1} + with tm.assert_produces_warning(FutureWarning, match=msg): + assert result.attrs == {"version": 1} @skip_if_no("jinja2") def test_inspect_getmembers(self):