Repository: pandas-dev/pandas
Pull request: EHN: read_spss stores the metadata in df.attrs
- [x] closes #54264
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Problem statement:
read_spss doens't return the metadata
### Feature Type

- [ ] Adding new functionality to pandas

- [X] Changing existing functionality in pandas

- [ ] Removing existing functionality in pandas


### Problem Description

`def read_spss(
    path: str | Path,
    usecols: Sequence[str] | None = None,
    convert_categoricals: bool = True,
) -> DataFrame:
    """
    Load an SPSS file from the file path, returning a DataFrame.

    .. versionadded:: 0.25.0

    Parameters
    ----------
    path : str or Path
        File path.
    usecols : list-like, optional
        Return a subset of the columns. If None, return all columns.
    convert_categoricals : bool, default is True
        Convert categorical columns into pd.Categorical.

    Returns
    -------
    DataFrame
    """
    pyreadstat = import_optional_dependency("pyreadstat")

    if usecols is not None:
        if not is_list_like(usecols):
            raise TypeError("usecols must be list-like.")
        else:
            usecols = list(usecols)  # pyreadstat requires a list

    df, _ = pyreadstat.read_sav(
        stringify_path(path), usecols=usecols, apply_value_formats=convert_categoricals
    )
    return df`
    
    pandas has this function, which uses pyreadstat to read 'sav' files and returns a dataset back, but for some reason it ignores the metadata, and I guess some may use this, otherwise why using spss at first place

### Feature Description

Return the metadata along with the dataframe

### Alternative Solutions

`def read_spss(
    path: str | Path,
    usecols: Sequence[str] | None = None,
    convert_categoricals: bool = True,
) -> DataFrame:
    """
    Load an SPSS file from the file path, returning a DataFrame.

    .. versionadded:: 0.25.0

    Parameters
    ----------
    path : str or Path
        File path.
    usecols : list-like, optional
        Return a subset of the columns. If None, return all columns.
    convert_categoricals : bool, default is True
        Convert categorical columns into pd.Categorical.

    Returns
    -------
    DataFrame
    """
    pyreadstat = import_optional_dependency("pyreadstat")

    if usecols is not None:
        if not is_list_like(usecols):
            raise TypeError("usecols must be list-like.")
        else:
            usecols = list(usecols)  # pyreadstat requires a list

    df, metadata = pyreadstat.read_sav(
        stringify_path(path), usecols=usecols, apply_value_formats=convert_categoricals
    )
    return df, metadata`

### Additional Context

_No response_

Hints from issue discussion:
The best way is probably to store that in ``df.attrs``.

Contributions are welcome if you'd like to take a look.
@lithomas1 Is it okay for me to work on this issue, or do other members need to approve this issue as well?

take
> @lithomas1 Is it okay for me to work on this issue, or do other members need to approve this issue as well?

Sure, feel free to give it a go. I think this has already been done with parquet, so it shouldn't be too controversial.

Commit message: filter warning

Patch to review:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index cfcc99bf5bda0..1d62a748a5fbb 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -76,6 +76,7 @@ Other enhancements
 
 - :attr:`Series.attrs` / :attr:`DataFrame.attrs` now uses a deepcopy for propagating ``attrs`` (:issue:`54134`).
 - :func:`read_csv` now supports ``on_bad_lines`` parameter with ``engine="pyarrow"``. (:issue:`54480`)
+- :func:`read_spss` now returns a :class:`DataFrame` that stores the metadata in :attr:`DataFrame.attrs`. (:issue:`54264`)
 - :meth:`ExtensionArray._explode` interface method added to allow extension type implementations of the ``explode`` method (:issue:`54833`)
 - :meth:`ExtensionArray.duplicated` added to allow extension type implementations of the ``duplicated`` method (:issue:`55255`)
 - DataFrame.apply now allows the usage of numba (via ``engine="numba"``) to JIT compile the passed function, allowing for potential speedups (:issue:`54666`)
diff --git a/pandas/io/spss.py b/pandas/io/spss.py
index 58487c6cd721b..db31a07df79e6 100644
--- a/pandas/io/spss.py
+++ b/pandas/io/spss.py
@@ -63,9 +63,10 @@ def read_spss(
             raise TypeError("usecols must be list-like.")
         usecols = list(usecols)  # pyreadstat requires a list
 
-    df, _ = pyreadstat.read_sav(
+    df, metadata = pyreadstat.read_sav(
         stringify_path(path), usecols=usecols, apply_value_formats=convert_categoricals
     )
+    df.attrs = metadata.__dict__
     if dtype_backend is not lib.no_default:
         df = df.convert_dtypes(dtype_backend=dtype_backend)
     return df
diff --git a/pandas/tests/io/test_spss.py b/pandas/tests/io/test_spss.py
index 5d8dce72d69c9..a916d7ea568b5 100644
--- a/pandas/tests/io/test_spss.py
+++ b/pandas/tests/io/test_spss.py
@@ -116,3 +116,34 @@ def test_invalid_dtype_backend():
     )
     with pytest.raises(ValueError, match=msg):
         pd.read_spss("test", dtype_backend="numpy")
+
+
+@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
+def test_spss_metadata(datapath):
+    # GH 54264
+    fname = datapath("io", "data", "spss", "labelled-num.sav")
+
+    df = pd.read_spss(fname)
+    assert df.attrs == {
+        "column_names": ["VAR00002"],
+        "column_labels": [None],
+        "column_names_to_labels": {"VAR00002": None},
+        "file_encoding": "UTF-8",
+        "number_columns": 1,
+        "number_rows": 1,
+        "variable_value_labels": {"VAR00002": {1.0: "This is one"}},
+        "value_labels": {"labels0": {1.0: "This is one"}},
+        "variable_to_label": {"VAR00002": "labels0"},
+        "notes": [],
+        "original_variable_types": {"VAR00002": "F8.0"},
+        "readstat_variable_types": {"VAR00002": "double"},
+        "table_name": None,
+        "missing_ranges": {},
+        "missing_user_values": {},
+        "variable_storage_width": {"VAR00002": 8},
+        "variable_display_width": {"VAR00002": 8},
+        "variable_alignment": {"VAR00002": "unknown"},
+        "variable_measure": {"VAR00002": "unknown"},
+        "file_label": None,
+        "file_format": "sav/zsav",
+    }


Code review comments:
diff_hunk: @@ -116,3 +116,34 @@ def test_invalid_dtype_backend():
     )
     with pytest.raises(ValueError, match=msg):
         pd.read_spss("test", dtype_backend="numpy")
+
+
+@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
+def test_spss_metadata(datapath):
+    # GH 54264
+    fname = datapath("io", "data", "spss", "labelled-num.sav")
+
+    df = pd.read_spss(fname)
+    assert df.attrs == {
path: pandas/tests/io/test_spss.py
text: @user1:
Could you make the dict a separate variable?

@author:
Sure. Done in the [commit](https://github.com/pandas-dev/pandas/pull/55472/commits/0c584b187e087f6a77a93046500eaa6a45a4242a).