Skip to content

Commit 545381f

Browse files
topper-123jreback
authored andcommitted
TYP: DataFrame.(index|columns) and Series.index (#31126)
1 parent bd6b395 commit 545381f

File tree

8 files changed

+60
-20
lines changed

8 files changed

+60
-20
lines changed

doc/source/whatsnew/v1.0.0.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,6 @@ Other API changes
687687
- :meth:`Series.str.__iter__` was deprecated and will be removed in future releases (:issue:`28277`).
688688
- Added ``<NA>`` to the list of default NA values for :meth:`read_csv` (:issue:`30821`)
689689

690-
691690
.. _whatsnew_100.api.documentation:
692691

693692
Documentation Improvements

doc/source/whatsnew/v1.1.0.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ Other API changes
5555
- :meth:`Series.describe` will now show distribution percentiles for ``datetime`` dtypes, statistics ``first`` and ``last``
5656
will now be ``min`` and ``max`` to match with numeric dtypes in :meth:`DataFrame.describe` (:issue:`30164`)
5757
-
58-
-
58+
59+
Backwards incompatible API changes
60+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
61+
- :meth:`DataFrame.swaplevels` now raises a ``TypeError`` if the axis is not a :class:`MultiIndex`.
62+
Previously a ``AttributeError`` was raised (:issue:`31126`)
63+
5964

6065
.. ---------------------------------------------------------------------------
6166

pandas/core/frame.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
from pandas._config import get_option
4040

41-
from pandas._libs import algos as libalgos, lib
41+
from pandas._libs import algos as libalgos, lib, properties
4242
from pandas._typing import Axes, Axis, Dtype, FilePathOrBuffer, Level, Renamer
4343
from pandas.compat import PY37
4444
from pandas.compat._optional import import_optional_dependency
@@ -91,8 +91,10 @@
9191
)
9292
from pandas.core.dtypes.generic import (
9393
ABCDataFrame,
94+
ABCDatetimeIndex,
9495
ABCIndexClass,
9596
ABCMultiIndex,
97+
ABCPeriodIndex,
9698
ABCSeries,
9799
)
98100
from pandas.core.dtypes.missing import isna, notna
@@ -394,6 +396,7 @@ class DataFrame(NDFrame):
394396
2 7 8 9
395397
"""
396398

399+
_internal_names_set = {"columns", "index"} | NDFrame._internal_names_set
397400
_typ = "dataframe"
398401

399402
@property
@@ -5293,9 +5296,15 @@ def swaplevel(self, i=-2, j=-1, axis=0) -> "DataFrame":
52935296
result = self.copy()
52945297

52955298
axis = self._get_axis_number(axis)
5299+
5300+
if not isinstance(result._get_axis(axis), ABCMultiIndex): # pragma: no cover
5301+
raise TypeError("Can only swap levels on a hierarchical axis.")
5302+
52965303
if axis == 0:
5304+
assert isinstance(result.index, ABCMultiIndex)
52975305
result.index = result.index.swaplevel(i, j)
52985306
else:
5307+
assert isinstance(result.columns, ABCMultiIndex)
52995308
result.columns = result.columns.swaplevel(i, j)
53005309
return result
53015310

@@ -5322,8 +5331,10 @@ def reorder_levels(self, order, axis=0) -> "DataFrame":
53225331
result = self.copy()
53235332

53245333
if axis == 0:
5334+
assert isinstance(result.index, ABCMultiIndex)
53255335
result.index = result.index.reorder_levels(order)
53265336
else:
5337+
assert isinstance(result.columns, ABCMultiIndex)
53275338
result.columns = result.columns.reorder_levels(order)
53285339
return result
53295340

@@ -8352,8 +8363,10 @@ def to_timestamp(self, freq=None, how="start", axis=0, copy=True) -> "DataFrame"
83528363

83538364
axis = self._get_axis_number(axis)
83548365
if axis == 0:
8366+
assert isinstance(self.index, (ABCDatetimeIndex, ABCPeriodIndex))
83558367
new_data.set_axis(1, self.index.to_timestamp(freq=freq, how=how))
83568368
elif axis == 1:
8369+
assert isinstance(self.columns, (ABCDatetimeIndex, ABCPeriodIndex))
83578370
new_data.set_axis(0, self.columns.to_timestamp(freq=freq, how=how))
83588371
else: # pragma: no cover
83598372
raise AssertionError(f"Axis must be 0 or 1. Got {axis}")
@@ -8386,8 +8399,10 @@ def to_period(self, freq=None, axis=0, copy=True) -> "DataFrame":
83868399

83878400
axis = self._get_axis_number(axis)
83888401
if axis == 0:
8402+
assert isinstance(self.index, ABCDatetimeIndex)
83898403
new_data.set_axis(1, self.index.to_period(freq=freq))
83908404
elif axis == 1:
8405+
assert isinstance(self.columns, ABCDatetimeIndex)
83918406
new_data.set_axis(0, self.columns.to_period(freq=freq))
83928407
else: # pragma: no cover
83938408
raise AssertionError(f"Axis must be 0 or 1. Got {axis}")
@@ -8490,6 +8505,15 @@ def isin(self, values) -> "DataFrame":
84908505
self.columns,
84918506
)
84928507

8508+
# ----------------------------------------------------------------------
8509+
# Add index and columns
8510+
index: "Index" = properties.AxisProperty(
8511+
axis=1, doc="The index (row labels) of the DataFrame."
8512+
)
8513+
columns: "Index" = properties.AxisProperty(
8514+
axis=0, doc="The column labels of the DataFrame."
8515+
)
8516+
84938517
# ----------------------------------------------------------------------
84948518
# Add plotting methods to DataFrame
84958519
plot = CachedAccessor("plot", pandas.plotting.PlotAccessor)

pandas/core/generic.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from pandas._config import config
3232

33-
from pandas._libs import Timestamp, iNaT, lib, properties
33+
from pandas._libs import Timestamp, iNaT, lib
3434
from pandas._typing import (
3535
Axis,
3636
Dtype,
@@ -333,18 +333,6 @@ def _setup_axes(cls, axes: List[str], docs: Dict[str, str]) -> None:
333333
cls._info_axis_number = info_axis
334334
cls._info_axis_name = axes[info_axis]
335335

336-
# setup the actual axis
337-
def set_axis(a, i):
338-
setattr(cls, a, properties.AxisProperty(i, docs.get(a, a)))
339-
cls._internal_names_set.add(a)
340-
341-
if axes_are_reversed:
342-
for i, a in cls._AXIS_NAMES.items():
343-
set_axis(a, 1 - i)
344-
else:
345-
for i, a in cls._AXIS_NAMES.items():
346-
set_axis(a, i)
347-
348336
def _construct_axes_dict(self, axes=None, **kwargs):
349337
"""Return an axes dictionary for myself."""
350338
d = {a: self._get_axis(a) for a in (axes or self._AXIS_ORDERS)}
@@ -5084,6 +5072,7 @@ def __finalize__(
50845072
self.attrs[name] = other.attrs[name]
50855073
# For subclasses using _metadata.
50865074
for name in self._metadata:
5075+
assert isinstance(name, str)
50875076
object.__setattr__(self, name, getattr(other, name, None))
50885077
return self
50895078

pandas/core/reshape/pivot.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ def pivot_table(
134134
table = agged.unstack(to_unstack)
135135

136136
if not dropna:
137-
if table.index.nlevels > 1:
137+
if isinstance(table.index, MultiIndex):
138138
m = MultiIndex.from_arrays(
139139
cartesian_product(table.index.levels), names=table.index.names
140140
)
141141
table = table.reindex(m, axis=0)
142142

143-
if table.columns.nlevels > 1:
143+
if isinstance(table.columns, MultiIndex):
144144
m = MultiIndex.from_arrays(
145145
cartesian_product(table.columns.levels), names=table.columns.names
146146
)
@@ -373,7 +373,7 @@ def _generate_marginal_results_without_values(
373373
):
374374
if len(cols) > 0:
375375
# need to "interleave" the margins
376-
margin_keys = []
376+
margin_keys: Union[List, Index] = []
377377

378378
def _all_key():
379379
if len(cols) == 1:

pandas/core/series.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from pandas._config import get_option
2424

25-
from pandas._libs import index as libindex, lib, reshape, tslibs
25+
from pandas._libs import index as libindex, lib, properties, reshape, tslibs
2626
from pandas._typing import Label
2727
from pandas.compat.numpy import function as nv
2828
from pandas.util._decorators import Appender, Substitution
@@ -46,6 +46,8 @@
4646
from pandas.core.dtypes.generic import (
4747
ABCDataFrame,
4848
ABCDatetimeIndex,
49+
ABCMultiIndex,
50+
ABCPeriodIndex,
4951
ABCSeries,
5052
ABCSparseArray,
5153
)
@@ -177,6 +179,7 @@ class Series(base.IndexOpsMixin, generic.NDFrame):
177179

178180
_name: Optional[Hashable]
179181
_metadata: List[str] = ["name"]
182+
_internal_names_set = {"index"} | generic.NDFrame._internal_names_set
180183
_accessors = {"dt", "cat", "str", "sparse"}
181184
_deprecations = (
182185
base.IndexOpsMixin._deprecations
@@ -3395,6 +3398,7 @@ def swaplevel(self, i=-2, j=-1, copy=True) -> "Series":
33953398
Series
33963399
Series with levels swapped in MultiIndex.
33973400
"""
3401+
assert isinstance(self.index, ABCMultiIndex)
33983402
new_index = self.index.swaplevel(i, j)
33993403
return self._constructor(self._values, index=new_index, copy=copy).__finalize__(
34003404
self
@@ -3419,6 +3423,7 @@ def reorder_levels(self, order) -> "Series":
34193423
raise Exception("Can only reorder levels on a hierarchical axis.")
34203424

34213425
result = self.copy()
3426+
assert isinstance(result.index, ABCMultiIndex)
34223427
result.index = result.index.reorder_levels(order)
34233428
return result
34243429

@@ -4496,6 +4501,7 @@ def to_timestamp(self, freq=None, how="start", copy=True) -> "Series":
44964501
if copy:
44974502
new_values = new_values.copy()
44984503

4504+
assert isinstance(self.index, (ABCDatetimeIndex, ABCPeriodIndex))
44994505
new_index = self.index.to_timestamp(freq=freq, how=how)
45004506
return self._constructor(new_values, index=new_index).__finalize__(self)
45014507

@@ -4520,9 +4526,16 @@ def to_period(self, freq=None, copy=True) -> "Series":
45204526
if copy:
45214527
new_values = new_values.copy()
45224528

4529+
assert isinstance(self.index, ABCDatetimeIndex)
45234530
new_index = self.index.to_period(freq=freq)
45244531
return self._constructor(new_values, index=new_index).__finalize__(self)
45254532

4533+
# ----------------------------------------------------------------------
4534+
# Add index and columns
4535+
index: "Index" = properties.AxisProperty(
4536+
axis=0, doc="The index (axis labels) of the Series."
4537+
)
4538+
45264539
# ----------------------------------------------------------------------
45274540
# Accessor Methods
45284541
# ----------------------------------------------------------------------

pandas/io/formats/format.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@
5757
is_timedelta64_dtype,
5858
)
5959
from pandas.core.dtypes.generic import (
60+
ABCDatetimeIndex,
6061
ABCIndexClass,
6162
ABCMultiIndex,
63+
ABCPeriodIndex,
6264
ABCSeries,
6365
ABCSparseArray,
66+
ABCTimedeltaIndex,
6467
)
6568
from pandas.core.dtypes.missing import isna, notna
6669

@@ -295,6 +298,9 @@ def _get_footer(self) -> str:
295298
footer = ""
296299

297300
if getattr(self.series.index, "freq", None) is not None:
301+
assert isinstance(
302+
self.series.index, (ABCDatetimeIndex, ABCPeriodIndex, ABCTimedeltaIndex)
303+
)
298304
footer += "Freq: {freq}".format(freq=self.series.index.freqstr)
299305

300306
if self.name is not False and name is not None:

pandas/tests/test_multilevel.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,10 @@ def test_swaplevel(self):
957957
exp = self.frame.swaplevel("first", "second").T
958958
tm.assert_frame_equal(swapped, exp)
959959

960+
msg = "Can only swap levels on a hierarchical axis."
961+
with pytest.raises(TypeError, match=msg):
962+
DataFrame(range(3)).swaplevel()
963+
960964
def test_reorder_levels(self):
961965
result = self.ymd.reorder_levels(["month", "day", "year"])
962966
expected = self.ymd.swaplevel(0, 1).swaplevel(1, 2)

0 commit comments

Comments
 (0)