Repository: pandas-dev/pandas (python)

Issue:
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_

Pull request description:
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.


Patch:
diff --git a/asv_bench/benchmarks/series_methods.py b/asv_bench/benchmarks/series_methods.py
index 2883691455..f52f7a4bef 100644
--- a/asv_bench/benchmarks/series_methods.py
+++ b/asv_bench/benchmarks/series_methods.py
@@ -28,9 +28,6 @@ class SeriesConstructor:
     def time_constructor_no_data(self):
         Series(data=None, index=self.idx)
 
-    def time_constructor_fastpath(self):
-        Series(self.array, index=self.idx2, name="name", fastpath=True)
-
 
 class ToFrame:
     params = [["int64", "datetime64[ns]", "category", "Int64"], [None, "foo"]]
diff --git a/doc/source/whatsnew/v2.1.2.rst b/doc/source/whatsnew/v2.1.2.rst
index 569864ba71..cc51e22265 100644
--- a/doc/source/whatsnew/v2.1.2.rst
+++ b/doc/source/whatsnew/v2.1.2.rst
@@ -16,6 +16,7 @@ Fixed regressions
 - Fixed bug where PDEP-6 warning about setting an item of an incompatible dtype was being shown when creating a new conditional column (:issue:`55025`)
 - Fixed regression in :meth:`DataFrame.join` where result has missing values and dtype is arrow backed string (:issue:`55348`)
 - Fixed regression in :meth:`DataFrame.resample` which was extrapolating back to ``origin`` when ``origin`` was outside its bounds (:issue:`55064`)
+- Fixed regression in :meth:`DataFrame.sort_index` which was not sorting correctly when the index was a sliced :class:`MultiIndex` (:issue:`55379`)
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_212.bug_fixes:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index cfcc99bf5b..29a2d5c0b5 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`)
@@ -256,6 +257,7 @@ Other Deprecations
 - Deprecated strings ``H``, ``S``, ``U``, and ``N`` denoting units in :func:`to_timedelta` (:issue:`52536`)
 - Deprecated strings ``H``, ``T``, ``S``, ``L``, ``U``, and ``N`` denoting units in :class:`Timedelta` (:issue:`52536`)
 - Deprecated strings ``T``, ``S``, ``L``, ``U``, and ``N`` denoting frequencies in :class:`Minute`, :class:`Second`, :class:`Milli`, :class:`Micro`, :class:`Nano` (:issue:`52536`)
+- Deprecated the ``fastpath`` keyword in the :class:`Series` constructor (:issue:`20110`)
 - Deprecated the extension test classes ``BaseNoReduceTests``, ``BaseBooleanReduceTests``, and ``BaseNumericReduceTests``, use ``BaseReduceTests`` instead (:issue:`54663`)
 - Deprecated the option ``mode.data_manager`` and the ``ArrayManager``; only the ``BlockManager`` will be available in future versions (:issue:`55043`)
 - Deprecating downcasting the results of :meth:`DataFrame.fillna`, :meth:`Series.fillna`, :meth:`DataFrame.ffill`, :meth:`Series.ffill`, :meth:`DataFrame.bfill`, :meth:`Series.bfill` in object-dtype cases. To opt in to the future version, use ``pd.set_option("future.no_silent_downcasting", True)`` (:issue:`54261`)
diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py
index b55a40f548..4bc30d6dbd 100644
--- a/pandas/core/arrays/interval.py
+++ b/pandas/core/arrays/interval.py
@@ -15,8 +15,6 @@ from typing import (
 
 import numpy as np
 
-from pandas._config import get_option
-
 from pandas._libs import lib
 from pandas._libs.interval import (
     VALID_CLOSED,
@@ -1233,43 +1231,10 @@ class IntervalArray(IntervalMixin, ExtensionArray):
     # ---------------------------------------------------------------------
     # Rendering Methods
 
-    def _format_data(self) -> str:
-        # TODO: integrate with categorical and make generic
-        n = len(self)
-        max_seq_items = min((get_option("display.max_seq_items") or n) // 10, 10)
-
-        formatter = str
-
-        if n == 0:
-            summary = "[]"
-        elif n == 1:
-            first = formatter(self[0])
-            summary = f"[{first}]"
-        elif n == 2:
-            first = formatter(self[0])
-            last = formatter(self[-1])
-            summary = f"[{first}, {last}]"
-        else:
-            if n > max_seq_items:
-                n = min(max_seq_items // 2, 10)
-                head = [formatter(x) for x in self[:n]]
-                tail = [formatter(x) for x in self[-n:]]
-                head_str = ", ".join(head)
-                tail_str = ", ".join(tail)
-                summary = f"[{head_str} ... {tail_str}]"
-            else:
-                tail = [formatter(x) for x in self]
-                tail_str = ", ".join(tail)
-                summary = f"[{tail_str}]"
-
-        return summary
-
-    def __repr__(self) -> str:
-        data = self._format_data()
-        class_name = f"<{type(self).__name__}>\n"
-
-        template = f"{class_name}{data}\nLength: {len(self)}, dtype: {self.dtype}"
-        return template
+    def _formatter(self, boxed: bool = False):
+        # returning 'str' here causes us to render as e.g. "(0, 1]" instead of
+        #  "Interval(0, 1, closed='right')"
+        return str
 
     # ---------------------------------------------------------------------
     # Vectorized Interval Properties/Attributes
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 7802657269..09c43822e1 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -5168,12 +5168,7 @@ class DataFrame(NDFrame, OpsMixin):
 
     @property
     def _series(self):
-        return {
-            item: Series(
-                self._mgr.iget(idx), index=self.index, name=item, fastpath=True
-            )
-            for idx, item in enumerate(self.columns)
-        }
+        return {item: self._ixs(idx, axis=1) for idx, item in enumerate(self.columns)}
 
     # ----------------------------------------------------------------------
     # Reindexing and alignment
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index 8955329a7a..c5cab225fa 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -107,7 +107,10 @@ from pandas.core.sorting import (
     lexsort_indexer,
 )
 
-from pandas.io.formats.printing import pprint_thing
+from pandas.io.formats.printing import (
+    get_adjustment,
+    pprint_thing,
+)
 
 if TYPE_CHECKING:
     from pandas import (
@@ -1437,8 +1440,6 @@ class MultiIndex(Index):
             )
 
         if adjoin:
-            from pandas.io.formats.format import get_adjustment
-
             adj = get_adjustment()
             return adj.adjoin(space, *result_levels).split("\n")
         else:
diff --git a/pandas/core/series.py b/pandas/core/series.py
index ae9f77a6c0..c2eea371dd 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -374,8 +374,18 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         dtype: Dtype | None = None,
         name=None,
         copy: bool | None = None,
-        fastpath: bool = False,
+        fastpath: bool | lib.NoDefault = lib.no_default,
     ) -> None:
+        if fastpath is not lib.no_default:
+            warnings.warn(
+                "The 'fastpath' keyword in pd.Series is deprecated and will "
+                "be removed in a future version.",
+                DeprecationWarning,
+                stacklevel=find_stack_level(),
+            )
+        else:
+            fastpath = False
+
         if (
             isinstance(data, (SingleBlockManager, SingleArrayManager))
             and index is None
@@ -1009,7 +1019,8 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         # axis kwarg is retained for compat with NDFrame method
         #  _slice is *always* positional
         mgr = self._mgr.get_slice(slobj, axis=axis)
-        out = self._constructor(mgr, fastpath=True)
+        out = self._constructor_from_mgr(mgr, axes=mgr.axes)
+        out._name = self._name
         return out.__finalize__(self)
 
     def __getitem__(self, key):
diff --git a/pandas/io/common.py b/pandas/io/common.py
index f255ea8197..d08612f4f0 100644
--- a/pandas/io/common.py
+++ b/pandas/io/common.py
@@ -68,8 +68,8 @@ from pandas.core.dtypes.common import (
     is_integer,
     is_list_like,
 )
+from pandas.core.dtypes.generic import ABCMultiIndex
 
-from pandas.core.indexes.api import MultiIndex
 from pandas.core.shared_docs import _shared_docs
 
 _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
@@ -91,6 +91,8 @@ if TYPE_CHECKING:
         WriteBuffer,
     )
 
+    from pandas import MultiIndex
+
 
 @dataclasses.dataclass
 class IOArgs:
@@ -1228,7 +1230,7 @@ def is_potential_multi_index(
 
     return bool(
         len(columns)
-        and not isinstance(columns, MultiIndex)
+        and not isinstance(columns, ABCMultiIndex)
         and all(isinstance(c, tuple) for c in columns if c not in list(index_col))
     )
 
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
index 1efd6f18dc..cac83e2a48 100644
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -7,7 +7,6 @@ from __future__ import annotations
 from collections.abc import (
     Generator,
     Hashable,
-    Iterable,
     Mapping,
     Sequence,
 )
@@ -26,7 +25,6 @@ from typing import (
     Final,
     cast,
 )
-from unicodedata import east_asian_width
 
 import numpy as np
 
@@ -226,7 +224,7 @@ class SeriesFormatter:
             float_format = get_option("display.float_format")
         self.float_format = float_format
         self.dtype = dtype
-        self.adj = get_adjustment()
+        self.adj = printing.get_adjustment()
 
         self._chk_truncate()
 
@@ -347,69 +345,6 @@ class SeriesFormatter:
         return str("".join(result))
 
 
-class _TextAdjustment:
-    def __init__(self) -> None:
-        self.encoding = get_option("display.encoding")
-
-    def len(self, text: str) -> int:
-        return len(text)
-
-    def justify(self, texts: Any, max_len: int, mode: str = "right") -> list[str]:
-        return printing.justify(texts, max_len, mode=mode)
-
-    def adjoin(self, space: int, *lists, **kwargs) -> str:
-        return printing.adjoin(
-            space, *lists, strlen=self.len, justfunc=self.justify, **kwargs
-        )
-
-
-class _EastAsianTextAdjustment(_TextAdjustment):
-    def __init__(self) -> None:
-        super().__init__()
-        if get_option("display.unicode.ambiguous_as_wide"):
-            self.ambiguous_width = 2
-        else:
-            self.ambiguous_width = 1
-
-        # Definition of East Asian Width
-        # https://unicode.org/reports/tr11/
-        # Ambiguous width can be changed by option
-        self._EAW_MAP = {"Na": 1, "N": 1, "W": 2, "F": 2, "H": 1}
-
-    def len(self, text: str) -> int:
-        """
-        Calculate display width considering unicode East Asian Width
-        """
-        if not isinstance(text, str):
-            return len(text)
-
-        return sum(
-            self._EAW_MAP.get(east_asian_width(c), self.ambiguous_width) for c in text
-        )
-
-    def justify(
-        self, texts: Iterable[str], max_len: int, mode: str = "right"
-    ) -> list[str]:
-        # re-calculate padding space per str considering East Asian Width
-        def _get_pad(t):
-            return max_len - self.len(t) + len(t)
-
-        if mode == "left":
-            return [x.ljust(_get_pad(x)) for x in texts]
-        elif mode == "center":
-            return [x.center(_get_pad(x)) for x in texts]
-        else:
-            return [x.rjust(_get_pad(x)) for x in texts]
-
-
-def get_adjustment() -> _TextAdjustment:
-    use_east_asian_width = get_option("display.unicode.east_asian_width")
-    if use_east_asian_width:
-        return _EastAsianTextAdjustment()
-    else:
-        return _TextAdjustment()
-
-
 def get_dataframe_repr_params() -> dict[str, Any]:
     """Get the parameters used to repr(dataFrame) calls using DataFrame.to_string.
 
@@ -529,7 +464,7 @@ class DataFrameFormatter:
 
         self.tr_frame = self.frame
         self.truncate()
-        self.adj = get_adjustment()
+        self.adj = printing.get_adjustment()
 
     def get_strcols(self) -> list[list[str]]:
         """
@@ -1789,13 +1724,13 @@ def _make_fixed_width(
     strings: list[str],
     justify: str = "right",
     minimum: int | None = None,
-    adj: _TextAdjustment | None = None,
+    adj: printing._TextAdjustment | None = None,
 ) -> list[str]:
     if len(strings) == 0 or justify == "all":
         return strings
 
     if adj is None:
-        adjustment = get_adjustment()
+        adjustment = printing.get_adjustment()
     else:
         adjustment = adj
 
diff --git a/pandas/io/formats/printing.py b/pandas/io/formats/printing.py
index b57797b7ec..2cc9368f88 100644
--- a/pandas/io/formats/printing.py
+++ b/pandas/io/formats/printing.py
@@ -15,11 +15,14 @@ from typing import (
     TypeVar,
     Union,
 )
+from unicodedata import east_asian_width
 
 from pandas._config import get_option
 
 from pandas.core.dtypes.inference import is_sequence
 
+from pandas.io.formats.console import get_console_size
+
 EscapeChars = Union[Mapping[str, str], Iterable[str]]
 _KT = TypeVar("_KT")
 _VT = TypeVar("_VT")
@@ -42,7 +45,7 @@ def adjoin(space: int, *lists: list[str], **kwargs) -> str:
         function used to justify str. Needed for unicode handling.
     """
     strlen = kwargs.pop("strlen", len)
-    justfunc = kwargs.pop("justfunc", justify)
+    justfunc = kwargs.pop("justfunc", _adj_justify)
 
     newLists = []
     lengths = [max(map(strlen, x)) + space for x in lists[:-1]]
@@ -57,7 +60,7 @@ def adjoin(space: int, *lists: list[str], **kwargs) -> str:
     return "\n".join("".join(lines) for lines in toJoin)
 
 
-def justify(texts: Iterable[str], max_len: int, mode: str = "right") -> list[str]:
+def _adj_justify(texts: Iterable[str], max_len: int, mode: str = "right") -> list[str]:
     """
     Perform ljust, center, rjust against string or list-like
     """
@@ -314,9 +317,6 @@ def format_object_summary(
     -------
     summary string
     """
-    from pandas.io.formats.console import get_console_size
-    from pandas.io.formats.format import get_adjustment
-
     display_width, _ = get_console_size()
     if display_width is None:
         display_width = get_option("display.width") or 80
@@ -501,3 +501,72 @@ class PrettyDict(dict[_KT, _VT]):
 
     def __repr__(self) -> str:
         return pprint_thing(self)
+
+
+class _TextAdjustment:
+    def __init__(self) -> None:
+        self.encoding = get_option("display.encoding")
+
+    def len(self, text: str) -> int:
+        return len(text)
+
+    def justify(self, texts: Any, max_len: int, mode: str = "right") -> list[str]:
+        """
+        Perform ljust, center, rjust against string or list-like
+        """
+        if mode == "left":
+            return [x.ljust(max_len) for x in texts]
+        elif mode == "center":
+            return [x.center(max_len) for x in texts]
+        else:
+            return [x.rjust(max_len) for x in texts]
+
+    def adjoin(self, space: int, *lists, **kwargs) -> str:
+        return adjoin(space, *lists, strlen=self.len, justfunc=self.justify, **kwargs)
+
+
+class _EastAsianTextAdjustment(_TextAdjustment):
+    def __init__(self) -> None:
+        super().__init__()
+        if get_option("display.unicode.ambiguous_as_wide"):
+            self.ambiguous_width = 2
+        else:
+            self.ambiguous_width = 1
+
+        # Definition of East Asian Width
+        # https://unicode.org/reports/tr11/
+        # Ambiguous width can be changed by option
+        self._EAW_MAP = {"Na": 1, "N": 1, "W": 2, "F": 2, "H": 1}
+
+    def len(self, text: str) -> int:
+        """
+        Calculate display width considering unicode East Asian Width
+        """
+        if not isinstance(text, str):
+            return len(text)
+
+        return sum(
+            self._EAW_MAP.get(east_asian_width(c), self.ambiguous_width) for c in text
+        )
+
+    def justify(
+        self, texts: Iterable[str], max_len: int, mode: str = "right"
+    ) -> list[str]:
+        # re-calculate padding space per str considering East Asian Width
+        def _get_pad(t):
+            return max_len - self.len(t) + len(t)
+
+        if mode == "left":
+            return [x.ljust(_get_pad(x)) for x in texts]
+        elif mode == "center":
+            return [x.center(_get_pad(x)) for x in texts]
+        else:
+            return [x.rjust(_get_pad(x)) for x in texts]
+
+
+def get_adjustment() -> _TextAdjustment:
+    use_east_asian_width = get_option("display.unicode.east_asian_width")
+    if use_east_asian_width:
+        return _EastAsianTextAdjustment()
+    else:
+        return _TextAdjustment()
diff --git a/pandas/io/spss.py b/pandas/io/spss.py
index 58487c6cd7..db31a07df7 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


Test patch:
diff --git a/pandas/tests/copy_view/test_constructors.py b/pandas/tests/copy_view/test_constructors.py
index af7e759902..31f80d300c 100644
--- a/pandas/tests/copy_view/test_constructors.py
+++ b/pandas/tests/copy_view/test_constructors.py
@@ -99,7 +99,9 @@ def test_series_from_series_with_reindex(using_copy_on_write):
 def test_series_from_array(using_copy_on_write, idx, dtype, fastpath, arr):
     if idx is None or dtype is not None:
         fastpath = False
-    ser = Series(arr, dtype=dtype, index=idx, fastpath=fastpath)
+    msg = "The 'fastpath' keyword in pd.Series is deprecated"
+    with tm.assert_produces_warning(DeprecationWarning, match=msg):
+        ser = Series(arr, dtype=dtype, index=idx, fastpath=fastpath)
     ser_orig = ser.copy()
     data = getattr(arr, "_data", arr)
     if using_copy_on_write:
@@ -157,7 +159,9 @@ def test_series_from_index_different_dtypes(using_copy_on_write):
 def test_series_from_block_manager(using_copy_on_write, idx, dtype, fastpath):
     ser = Series([1, 2, 3], dtype="int64")
     ser_orig = ser.copy()
-    ser2 = Series(ser._mgr, dtype=dtype, fastpath=fastpath, index=idx)
+    msg = "The 'fastpath' keyword in pd.Series is deprecated"
+    with tm.assert_produces_warning(DeprecationWarning, match=msg):
+        ser2 = Series(ser._mgr, dtype=dtype, fastpath=fastpath, index=idx)
     assert np.shares_memory(get_array(ser), get_array(ser2))
     if using_copy_on_write:
         assert not ser2._mgr._has_no_reference(0)
diff --git a/pandas/tests/frame/methods/test_sort_index.py b/pandas/tests/frame/methods/test_sort_index.py
index 985a9e3602..f0222e5cec 100644
--- a/pandas/tests/frame/methods/test_sort_index.py
+++ b/pandas/tests/frame/methods/test_sort_index.py
@@ -955,3 +955,42 @@ class TestDataFrameSortIndexKey:
             )
 
         tm.assert_frame_equal(result, expected)
+
+
+def test_sort_index_with_sliced_multiindex():
+    # GH 55379
+    mi = MultiIndex.from_tuples(
+        [
+            ("a", "10"),
+            ("a", "18"),
+            ("a", "25"),
+            ("b", "16"),
+            ("b", "26"),
+            ("a", "45"),
+            ("b", "28"),
+            ("a", "5"),
+            ("a", "50"),
+            ("a", "51"),
+            ("b", "4"),
+        ],
+        names=["group", "str"],
+    )
+
+    df = DataFrame({"x": range(len(mi))}, index=mi)
+    result = df.iloc[0:6].sort_index()
+
+    expected = DataFrame(
+        {"x": [0, 1, 2, 5, 3, 4]},
+        index=MultiIndex.from_tuples(
+            [
+                ("a", "10"),
+                ("a", "18"),
+                ("a", "25"),
+                ("a", "45"),
+                ("b", "16"),
+                ("b", "26"),
+            ],
+            names=["group", "str"],
+        ),
+    )
+    tm.assert_frame_equal(result, expected)
diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py
index 585958765f..57f1f08270 100644
--- a/pandas/tests/io/formats/test_format.py
+++ b/pandas/tests/io/formats/test_format.py
@@ -212,7 +212,7 @@ class TestDataFrameFormatting:
             r = repr(df)
             r = r[r.find("\n") + 1 :]
 
-            adj = fmt.get_adjustment()
+            adj = printing.get_adjustment()
 
             for line, value in zip(r.split("\n"), df["B"]):
                 if adj.len(value) + 1 > max_len:
diff --git a/pandas/tests/io/formats/test_printing.py b/pandas/tests/io/formats/test_printing.py
index 492657587a..78198bce71 100644
--- a/pandas/tests/io/formats/test_printing.py
+++ b/pandas/tests/io/formats/test_printing.py
@@ -8,7 +8,6 @@ import pandas._config.config as cf
 import pandas as pd
 
 from pandas.io.formats import printing
-import pandas.io.formats.format as fmt
 
 
 def test_adjoin():
@@ -48,7 +47,7 @@ class TestFormattBase:
         adjoined = printing.adjoin(2, *data)
         assert adjoined == expected
 
-        adj = fmt._EastAsianTextAdjustment()
+        adj = printing._EastAsianTextAdjustment()
 
         expected = """あ  dd    ggg
 b   ええ  hhh
@@ -73,7 +72,7 @@ c        ff         いいい"""
         assert adj.len(cols[2]) == 26
 
     def test_justify(self):
-        adj = fmt._EastAsianTextAdjustment()
+        adj = printing._EastAsianTextAdjustment()
 
         def just(x, *args, **kwargs):
             # wrapper to test single str
@@ -95,7 +94,7 @@ c        ff         いいい"""
         assert just("パンダ", 10, mode="right") == "    パンダ"
 
     def test_east_asian_len(self):
-        adj = fmt._EastAsianTextAdjustment()
+        adj = printing._EastAsianTextAdjustment()
 
         assert adj.len("abc") == 3
         assert adj.len("abc") == 3
@@ -106,11 +105,11 @@ c        ff         いいい"""
         assert adj.len("ﾊﾟﾝﾀﾞpanda") == 10
 
     def test_ambiguous_width(self):
-        adj = fmt._EastAsianTextAdjustment()
+        adj = printing._EastAsianTextAdjustment()
         assert adj.len("¡¡ab") == 4
 
         with cf.option_context("display.unicode.ambiguous_as_wide", True):
-            adj = fmt._EastAsianTextAdjustment()
+            adj = printing._EastAsianTextAdjustment()
             assert adj.len("¡¡ab") == 6
 
         data = [["あ", "b", "c"], ["dd", "ええ", "ff"], ["ggg", "¡¡ab", "いいい"]]
diff --git a/pandas/tests/io/test_spss.py b/pandas/tests/io/test_spss.py
index 5d8dce72d6..b612b64e3b 100644
--- a/pandas/tests/io/test_spss.py
+++ b/pandas/tests/io/test_spss.py
@@ -116,3 +116,35 @@ 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)
+    metadata = {
+        "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",
+    }
+    assert df.attrs == metadata