Skip to content

Commit 2f3b0ed

Browse files
authored
DEPR: Index.format (#55439)
* REF: implement Index._format_flat, _format_multi * de-duplicate, change keyword * DEPR: Index.format * add formatter kwd * Post-merge fixup
1 parent abba4e2 commit 2f3b0ed

File tree

20 files changed

+259
-63
lines changed

20 files changed

+259
-63
lines changed

doc/source/whatsnew/v2.2.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ For example:
234234
Other Deprecations
235235
^^^^^^^^^^^^^^^^^^
236236
- Changed :meth:`Timedelta.resolution_string` to return ``h``, ``min``, ``s``, ``ms``, ``us``, and ``ns`` instead of ``H``, ``T``, ``S``, ``L``, ``U``, and ``N``, for compatibility with respective deprecations in frequency aliases (:issue:`52536`)
237+
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
237238
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`)
238239
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`)
239240
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_dict`. (:issue:`54229`)
@@ -261,6 +262,7 @@ Other Deprecations
261262
- Deprecated the extension test classes ``BaseNoReduceTests``, ``BaseBooleanReduceTests``, and ``BaseNumericReduceTests``, use ``BaseReduceTests`` instead (:issue:`54663`)
262263
- Deprecated the option ``mode.data_manager`` and the ``ArrayManager``; only the ``BlockManager`` will be available in future versions (:issue:`55043`)
263264
- 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`)
265+
-
264266

265267
.. ---------------------------------------------------------------------------
266268
.. _whatsnew_220.performance:

pandas/core/indexes/base.py

+33
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,14 @@ def format(
13751375
"""
13761376
Render a string representation of the Index.
13771377
"""
1378+
warnings.warn(
1379+
# GH#55413
1380+
f"{type(self).__name__}.format is deprecated and will be removed "
1381+
"in a future version. Convert using index.astype(str) or "
1382+
"index.map(formatter) instead.",
1383+
FutureWarning,
1384+
stacklevel=find_stack_level(),
1385+
)
13781386
header = []
13791387
if name:
13801388
header.append(
@@ -1388,6 +1396,31 @@ def format(
13881396

13891397
return self._format_with_header(header=header, na_rep=na_rep)
13901398

1399+
_default_na_rep = "NaN"
1400+
1401+
@final
1402+
def _format_flat(
1403+
self,
1404+
*,
1405+
include_name: bool,
1406+
formatter: Callable | None = None,
1407+
) -> list[str_t]:
1408+
"""
1409+
Render a string representation of the Index.
1410+
"""
1411+
header = []
1412+
if include_name:
1413+
header.append(
1414+
pprint_thing(self.name, escape_chars=("\t", "\r", "\n"))
1415+
if self.name is not None
1416+
else ""
1417+
)
1418+
1419+
if formatter is not None:
1420+
return header + list(self.map(formatter))
1421+
1422+
return self._format_with_header(header=header, na_rep=self._default_na_rep)
1423+
13911424
def _format_with_header(self, *, header: list[str_t], na_rep: str_t) -> list[str_t]:
13921425
from pandas.io.formats.format import format_array
13931426

pandas/core/indexes/datetimelike.py

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
cast,
1515
final,
1616
)
17+
import warnings
1718

1819
import numpy as np
1920

@@ -42,6 +43,7 @@
4243
cache_readonly,
4344
doc,
4445
)
46+
from pandas.util._exceptions import find_stack_level
4547

4648
from pandas.core.dtypes.common import (
4749
is_integer,
@@ -187,6 +189,7 @@ def _convert_tolerance(self, tolerance, target):
187189

188190
# --------------------------------------------------------------------
189191
# Rendering Methods
192+
_default_na_rep = "NaT"
190193

191194
def format(
192195
self,
@@ -198,6 +201,14 @@ def format(
198201
"""
199202
Render a string representation of the Index.
200203
"""
204+
warnings.warn(
205+
# GH#55413
206+
f"{type(self).__name__}.format is deprecated and will be removed "
207+
"in a future version. Convert using index.astype(str) or "
208+
"index.map(formatter) instead.",
209+
FutureWarning,
210+
stacklevel=find_stack_level(),
211+
)
201212
header = []
202213
if name:
203214
header.append(

pandas/core/indexes/multi.py

+72
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,15 @@ def format(
14301430
sparsify=None,
14311431
adjoin: bool = True,
14321432
) -> list:
1433+
warnings.warn(
1434+
# GH#55413
1435+
f"{type(self).__name__}.format is deprecated and will be removed "
1436+
"in a future version. Convert using index.astype(str) or "
1437+
"index.map(formatter) instead.",
1438+
FutureWarning,
1439+
stacklevel=find_stack_level(),
1440+
)
1441+
14331442
if name is not None:
14341443
names = name
14351444

@@ -1492,6 +1501,69 @@ def format(
14921501
else:
14931502
return result_levels
14941503

1504+
def _format_multi(
1505+
self,
1506+
*,
1507+
include_names: bool,
1508+
sparsify: bool | None | lib.NoDefault,
1509+
formatter: Callable | None = None,
1510+
) -> list:
1511+
if len(self) == 0:
1512+
return []
1513+
1514+
stringified_levels = []
1515+
for lev, level_codes in zip(self.levels, self.codes):
1516+
na = _get_na_rep(lev.dtype)
1517+
1518+
if len(lev) > 0:
1519+
taken = formatted = lev.take(level_codes)
1520+
formatted = taken._format_flat(include_name=False, formatter=formatter)
1521+
1522+
# we have some NA
1523+
mask = level_codes == -1
1524+
if mask.any():
1525+
formatted = np.array(formatted, dtype=object)
1526+
formatted[mask] = na
1527+
formatted = formatted.tolist()
1528+
1529+
else:
1530+
# weird all NA case
1531+
formatted = [
1532+
pprint_thing(na if isna(x) else x, escape_chars=("\t", "\r", "\n"))
1533+
for x in algos.take_nd(lev._values, level_codes)
1534+
]
1535+
stringified_levels.append(formatted)
1536+
1537+
result_levels = []
1538+
for lev, lev_name in zip(stringified_levels, self.names):
1539+
level = []
1540+
1541+
if include_names:
1542+
level.append(
1543+
pprint_thing(lev_name, escape_chars=("\t", "\r", "\n"))
1544+
if lev_name is not None
1545+
else ""
1546+
)
1547+
1548+
level.extend(np.array(lev, dtype=object))
1549+
result_levels.append(level)
1550+
1551+
if sparsify is None:
1552+
sparsify = get_option("display.multi_sparse")
1553+
1554+
if sparsify:
1555+
sentinel: Literal[""] | bool | lib.NoDefault = ""
1556+
# GH3547 use value of sparsify as sentinel if it's "Falsey"
1557+
assert isinstance(sparsify, bool) or sparsify is lib.no_default
1558+
if sparsify is lib.no_default:
1559+
sentinel = sparsify
1560+
# little bit of a kludge job for #1217
1561+
result_levels = sparsify_labels(
1562+
result_levels, start=int(include_names), sentinel=sentinel
1563+
)
1564+
1565+
return result_levels
1566+
14951567
# --------------------------------------------------------------------
14961568
# Names Methods
14971569

pandas/io/formats/excel.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,8 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
623623
return
624624

625625
columns = self.columns
626-
level_strs = columns.format(
627-
sparsify=self.merge_cells, adjoin=False, names=False
626+
level_strs = columns._format_multi(
627+
sparsify=self.merge_cells, include_names=False
628628
)
629629
level_lengths = get_level_lengths(level_strs)
630630
coloffset = 0
@@ -813,8 +813,8 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
813813

814814
if self.merge_cells:
815815
# Format hierarchical rows as merged cells.
816-
level_strs = self.df.index.format(
817-
sparsify=True, adjoin=False, names=False
816+
level_strs = self.df.index._format_multi(
817+
sparsify=True, include_names=False
818818
)
819819
level_lengths = get_level_lengths(level_strs)
820820

pandas/io/formats/format.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,14 @@ def to_string(self) -> str:
313313
if len(series) == 0:
314314
return f"{type(self.series).__name__}([], {footer})"
315315

316-
have_header = _has_names(series.index)
317-
fmt_index = self.tr_series.index.format(name=True)
316+
index = series.index
317+
have_header = _has_names(index)
318+
if isinstance(index, MultiIndex):
319+
fmt_index = index._format_multi(include_names=True, sparsify=None)
320+
adj = printing.get_adjustment()
321+
fmt_index = adj.adjoin(2, *fmt_index).split("\n")
322+
else:
323+
fmt_index = index._format_flat(include_name=True)
318324
fmt_values = self._get_formatted_values()
319325

320326
if self.is_truncated_vertically:
@@ -777,7 +783,7 @@ def _get_formatted_column_labels(self, frame: DataFrame) -> list[list[str]]:
777783
columns = frame.columns
778784

779785
if isinstance(columns, MultiIndex):
780-
fmt_columns = columns.format(sparsify=False, adjoin=False)
786+
fmt_columns = columns._format_multi(sparsify=False, include_names=False)
781787
fmt_columns = list(zip(*fmt_columns))
782788
dtypes = self.frame.dtypes._values
783789

@@ -802,7 +808,7 @@ def space_format(x, y):
802808

803809
str_columns = [list(x) for x in zip(*str_columns)]
804810
else:
805-
fmt_columns = columns.format()
811+
fmt_columns = columns._format_flat(include_name=False)
806812
dtypes = self.frame.dtypes
807813
need_leadsp = dict(zip(fmt_columns, map(is_numeric_dtype, dtypes)))
808814
str_columns = [
@@ -821,14 +827,15 @@ def _get_formatted_index(self, frame: DataFrame) -> list[str]:
821827
fmt = self._get_formatter("__index__")
822828

823829
if isinstance(index, MultiIndex):
824-
fmt_index = index.format(
830+
fmt_index = index._format_multi(
825831
sparsify=self.sparsify,
826-
adjoin=False,
827-
names=self.show_row_idx_names,
832+
include_names=self.show_row_idx_names,
828833
formatter=fmt,
829834
)
830835
else:
831-
fmt_index = [index.format(name=self.show_row_idx_names, formatter=fmt)]
836+
fmt_index = [
837+
index._format_flat(include_name=self.show_row_idx_names, formatter=fmt)
838+
]
832839

833840
fmt_index = [
834841
tuple(

pandas/io/formats/html.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def _write_col_header(self, indent: int) -> None:
282282
sentinel = lib.no_default
283283
else:
284284
sentinel = False
285-
levels = self.columns.format(sparsify=sentinel, adjoin=False, names=False)
285+
levels = self.columns._format_multi(sparsify=sentinel, include_names=False)
286286
level_lengths = get_level_lengths(levels, sentinel)
287287
inner_lvl = len(level_lengths) - 1
288288
for lnum, (records, values) in enumerate(zip(level_lengths, levels)):
@@ -437,7 +437,8 @@ def _write_regular_rows(
437437
if fmt is not None:
438438
index_values = self.fmt.tr_frame.index.map(fmt)
439439
else:
440-
index_values = self.fmt.tr_frame.index.format()
440+
# only reached with non-Multi index
441+
index_values = self.fmt.tr_frame.index._format_flat(include_name=False)
441442

442443
row: list[str] = []
443444
for i in range(nrows):
@@ -480,13 +481,13 @@ def _write_hierarchical_rows(
480481
nrows = len(frame)
481482

482483
assert isinstance(frame.index, MultiIndex)
483-
idx_values = frame.index.format(sparsify=False, adjoin=False, names=False)
484+
idx_values = frame.index._format_multi(sparsify=False, include_names=False)
484485
idx_values = list(zip(*idx_values))
485486

486487
if self.fmt.sparsify:
487488
# GH3547
488489
sentinel = lib.no_default
489-
levels = frame.index.format(sparsify=sentinel, adjoin=False, names=False)
490+
levels = frame.index._format_multi(sparsify=sentinel, include_names=False)
490491

491492
level_lengths = get_level_lengths(levels, sentinel)
492493
inner_lvl = len(level_lengths) - 1
@@ -579,7 +580,7 @@ def _write_hierarchical_rows(
579580
)
580581

581582
idx_values = list(
582-
zip(*frame.index.format(sparsify=False, adjoin=False, names=False))
583+
zip(*frame.index._format_multi(sparsify=False, include_names=False))
583584
)
584585
row = []
585586
row.extend(idx_values[i])
@@ -606,7 +607,8 @@ def _get_formatted_values(self) -> dict[int, list[str]]:
606607
return {i: self.fmt.format_col(i) for i in range(self.ncols)}
607608

608609
def _get_columns_formatted_values(self) -> list[str]:
609-
return self.columns.format()
610+
# only reached with non-Multi Index
611+
return self.columns._format_flat(include_name=False)
610612

611613
def write_style(self) -> None:
612614
# We use the "scoped" attribute here so that the desired

pandas/io/formats/style_render.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1652,9 +1652,9 @@ def _get_level_lengths(
16521652
Result is a dictionary of (level, initial_position): span
16531653
"""
16541654
if isinstance(index, MultiIndex):
1655-
levels = index.format(sparsify=lib.no_default, adjoin=False)
1655+
levels = index._format_multi(sparsify=lib.no_default, include_names=False)
16561656
else:
1657-
levels = index.format()
1657+
levels = index._format_flat(include_name=False)
16581658

16591659
if hidden_elements is None:
16601660
hidden_elements = []

pandas/tests/indexes/base_class/test_formats.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pandas._config.config as cf
55

66
from pandas import Index
7+
import pandas._testing as tm
78

89

910
class TestIndexRendering:
@@ -133,7 +134,9 @@ def test_summary_bug(self):
133134
def test_index_repr_bool_nan(self):
134135
# GH32146
135136
arr = Index([True, False, np.nan], dtype=object)
136-
exp1 = arr.format()
137+
msg = "Index.format is deprecated"
138+
with tm.assert_produces_warning(FutureWarning, match=msg):
139+
exp1 = arr.format()
137140
out1 = ["True", "False", "NaN"]
138141
assert out1 == exp1
139142

@@ -145,4 +148,6 @@ def test_format_different_scalar_lengths(self):
145148
# GH#35439
146149
idx = Index(["aaaaaaaaa", "b"])
147150
expected = ["aaaaaaaaa", "b"]
148-
assert idx.format() == expected
151+
msg = r"Index\.format is deprecated"
152+
with tm.assert_produces_warning(FutureWarning, match=msg):
153+
assert idx.format() == expected

pandas/tests/indexes/categorical/test_formats.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
import pandas._config.config as cf
55

66
from pandas import CategoricalIndex
7+
import pandas._testing as tm
78

89

910
class TestCategoricalIndexRepr:
1011
def test_format_different_scalar_lengths(self):
1112
# GH#35439
1213
idx = CategoricalIndex(["aaaaaaaaa", "b"])
1314
expected = ["aaaaaaaaa", "b"]
14-
assert idx.format() == expected
15+
msg = r"CategoricalIndex\.format is deprecated"
16+
with tm.assert_produces_warning(FutureWarning, match=msg):
17+
assert idx.format() == expected
1518

1619
def test_string_categorical_index_repr(self):
1720
# short
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
""" generic tests from the Datetimelike class """
22
from pandas import date_range
3+
import pandas._testing as tm
34

45

56
class TestDatetimeIndex:
67
def test_format(self):
78
# GH35439
89
idx = date_range("20130101", periods=5)
910
expected = [f"{x:%Y-%m-%d}" for x in idx]
10-
assert idx.format() == expected
11+
msg = r"DatetimeIndex\.format is deprecated"
12+
with tm.assert_produces_warning(FutureWarning, match=msg):
13+
assert idx.format() == expected

0 commit comments

Comments
 (0)