pandas-dev/pandas · #70499
BUG: Index.copy treating name as a list of names (GH#19171)
doc/source/whatsnew/v3.2.0.rst2 + / 0 −
@@ -116,6 +116,7 @@ Other deprecations - Deprecated ``median``, ``min``, ``max``, ``idxmin``, ``idxmax``, ``argmin``, ``argmax``, ``cummin``, ``cummax``, ``clip``, and the comparisons ``<``, ``<=``, ``>``, ``>=`` on ``complex`` data; complex numbers have no natural ordering, so in a future version these will raise ``TypeError`` (:issue:`43770`, :issue:`70087`) - Deprecated ``pandas.to_pickle``, use the :meth:`DataFrame.to_pickle` or :meth:`Series.to_pickle` method instead (:issue:`48402`) - Deprecated passing ``None`` entries in ``objs`` to :func:`concat`; filter them out before calling :func:`concat` instead (:issue:`57846`)+- Deprecated passing a list-like ``name`` to :meth:`Index.copy`; pass the label itself instead (:issue:`19171`) - Deprecated passing a negative ``min_count`` to :meth:`DataFrame.sum`, :meth:`DataFrame.prod`, :meth:`Series.sum`, :meth:`Series.prod`, :meth:`TimedeltaIndex.sum`, the ``sum`` and ``prod`` methods of pandas arrays, and the ``sum``, ``prod``, ``min``, ``max``, ``first``, and ``last`` methods of :class:`.DataFrameGroupBy`, :class:`.SeriesGroupBy`, and :class:`.Resampler`. Pass ``min_count=0`` instead, which behaves the same (:issue:`50022`) - Deprecated the ``cache_dates`` argument in :func:`read_csv`, :func:`read_table`, and :func:`read_fwf` (:issue:`68705`) - Deprecated the ``columns`` keyword in :meth:`Series.drop`, which was silently ignored; use ``index`` or ``labels`` instead (:issue:`39509`)@@ -203,6 +204,7 @@ Indexing - Bug in :meth:`DataFrame.loc` and :meth:`Series.loc` raising ``KeyError`` for a ``True`` or ``False`` label present in an object-dtype :class:`Index` or the first level of a :class:`MultiIndex` (:issue:`50165`) - Bug in :meth:`DataFrame.loc` and :meth:`Series.loc` setitem with an integer slice raising ``TypeError`` where the same ``.loc`` getitem succeeds, e.g. on an index of :class:`decimal.Decimal` values (:issue:`70219`) - Bug in :meth:`DataFrame.loc` and :meth:`Series.loc` slicing with an integer or float bound that is not in the index: an object-dtype index of numbers (e.g. :class:`decimal.Decimal`) raised ``TypeError`` instead of slicing by sort order, and a non-monotonic index of non-string values raised ``TypeError`` instead of ``KeyError`` (:issue:`70295`)+- Bug in :meth:`Index.copy` treating a tuple ``name`` as a list of names (:issue:`19171`) - Bug in :meth:`Index.difference` dropping elements that are not in ``other`` when ``other`` was a :class:`DatetimeIndex`, :class:`TimedeltaIndex` or :class:`PeriodIndex` and the elements were strings that parse to its values, or when ``other`` was an :class:`IntervalIndex` and the elements were scalars contained in its intervals (:issue:`58971`) Missingpandas/core/indexes/base.py24 + / 37 −
@@ -1,6 +1,7 @@ from __future__ import annotations from collections import abc+from copy import deepcopy from datetime import ( datetime, tzinfo,@@ -1417,14 +1418,36 @@ def copy( False """ - name = self._validate_names(name=name, deep=deep)[0]+ name = self._validate_copy_name(name, deep) if deep: new_data = self._data.copy() new_index = type(self)._simple_new(new_data, name=name) else: new_index = self._rename(name=name) return new_index + @final+ def _validate_copy_name(self, name: Hashable, deep: bool) -> Hashable:+ """+ Validate the ``name`` passed to ``copy``, defaulting to ``self.name``.+ """+ if name is None:+ return deepcopy(self.name) if deep else self.name+ if is_list_like(name) and not is_hashable(name):+ labels = list(cast("Iterable[Hashable]", name))+ if len(labels) == 1:+ # GH#19171+ warnings.warn(+ f"Passing a list-like 'name' to {type(self).__name__}.copy is "+ "deprecated and will raise in a future version. Pass the label "+ "itself instead.",+ Pandas4Warning,+ stacklevel=find_stack_level(),+ )+ name = labels[0]+ validate_all_hashable(name, error_name=f"{type(self).__name__}.name")+ return name+ @final def __copy__(self) -> Self: return self.copy(deep=False)@@ -1880,42 +1903,6 @@ def name(self, value: Hashable) -> None: maybe_extract_name(value, None, type(self)) self._name = value - @final- def _validate_names(- self,- name: Hashable = None,- names: Sequence[Hashable] | None = None,- deep: bool = False,- ) -> list[Hashable]:- """- Handles the quirks of having a singular 'name' parameter for general- Index and plural 'names' parameter for MultiIndex.- """- from copy import deepcopy-- if names is not None and name is not None:- raise TypeError("Can only provide one of `names` and `name`")- if names is None and name is None:- new_names = deepcopy(self.names) if deep else self.names- elif names is not None:- if not is_list_like(names):- raise TypeError("Must pass list-like as `names`.")- new_names = names- elif not is_list_like(name):- new_names = [name]- else:- new_names = name-- if len(new_names) != len(self.names): # pyright: ignore[reportArgumentType]- raise ValueError(- f"Length of new names must be {len(self.names)}, got {len(new_names)}" # pyright: ignore[reportArgumentType]- )-- # All items in 'new_names' need to be hashable- validate_all_hashable(*new_names, error_name=f"{type(self).__name__}.name") # pyright: ignore[reportOptionalIterable, reportGeneralTypeIssues]-- return new_names # pyright: ignore[reportReturnType]- def _get_default_index_names( self, names: Sequence[Hashable] | int | str_t | None = None,pandas/core/indexes/multi.py30 + / 3 −
@@ -8,6 +8,7 @@ Iterable, Sequence, )+from copy import deepcopy from functools import wraps from itertools import zip_longest from sys import getsizeof@@ -1377,6 +1378,31 @@ def _view(self) -> MultiIndex: # -------------------------------------------------------------------- + def _validate_names(+ self,+ name: Hashable = None,+ names: Sequence[Hashable] | None = None,+ ) -> list[Hashable]:+ """+ Validate the ``name`` or ``names`` passed to ``copy``.+ """+ if names is not None and name is not None:+ raise TypeError("Can only provide one of `names` and `name`")+ if names is not None:+ if not is_list_like(names):+ raise TypeError("Must pass list-like as `names`.")+ new_names = list(names)+ elif not is_list_like(name):+ new_names = [name]+ else:+ new_names = list(cast("Iterable[Hashable]", name))++ if len(new_names) != self.nlevels:+ raise ValueError(+ f"Length of new names must be {self.nlevels}, got {len(new_names)}"+ )+ return new_names+ # error: Signature of "copy" incompatible with supertype "Index" def copy( # type: ignore[override] self,@@ -1433,13 +1459,14 @@ def copy( # type: ignore[override] MultiIndex([('a', 'b', 'c')], ) """- names = self._validate_names(name=name, names=names, deep=deep)+ if names is None and name is None:+ names = deepcopy(self.names) if deep else self.names+ else:+ names = self._validate_names(name=name, names=names) keep_id = not deep levels, codes = None, None if deep:- from copy import deepcopy- levels = deepcopy(self.levels) codes = deepcopy(self.codes) pandas/core/indexes/range.py1 + / 1 −
@@ -701,7 +701,7 @@ def copy(self, name: Hashable | None = None, deep: bool = False) -> Self: >>> idx is new_idx False """- name = self._validate_names(name=name, deep=deep)[0]+ name = self._validate_copy_name(name, deep) new_index = self._rename(name=name) return new_index pandas/tests/indexes/ranges/test_constructors.py7 + / 0 −
@@ -139,6 +139,13 @@ def test_constructor_name(self): assert copy.name == "copy" assert new.name == "new" + def test_constructor_tuple_name(self):+ # GH#19171+ orig = pd.RangeIndex(2, name=("a", "b"))+ assert pd.Index(orig).name == ("a", "b")+ assert pd.RangeIndex(orig).name == ("a", "b")+ assert pd.RangeIndex(orig, name=("x",)).name == ("x",)+ def test_constructor_corner(self): arr = np.array([1, 2, 3, 4], dtype=object) index = pd.RangeIndex(1, 5)pandas/tests/indexes/test_common.py10 + / 3 −
@@ -171,12 +171,19 @@ def test_copy_name2(self, index_flat): assert index.copy(name="mario").name == "mario" - with pytest.raises(ValueError, match="Length of new names must be 1, got 2"):- index.copy(name=["mario", "luigi"])+ # GH#19171 hashable list-likes are valid names, not a list of names+ assert index.copy(name=("mario", "luigi")).name == ("mario", "luigi")+ assert index.copy(name=("mario",)).name == ("mario",)+ assert index.copy(name=("mario",), deep=True).name == ("mario",)+ assert index.copy(name=frozenset({"mario"})).name == frozenset({"mario"})++ msg = "Passing a list-like 'name'"+ with tm.assert_produces_warning(Pandas4Warning, match=msg):+ assert index.copy(name=["mario"]).name == "mario" msg = f"{type(index).__name__}.name must be a hashable type" with pytest.raises(TypeError, match=msg):- index.copy(name=[["mario"]])+ index.copy(name=["mario", "luigi"]) def test_unique_level(self, index_flat): # don't test a MultiIndex here (as its tested separated)