From a91d256be215d606c8c5bafa5a505a8381390370 Mon Sep 17 00:00:00 2001 From: tp Date: Sat, 8 Aug 2020 13:36:43 +0100 Subject: [PATCH 01/10] corrections --- pandas/tests/test_multilevel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index 1ba73292dc0b4..724558bd49ea2 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -63,8 +63,8 @@ def setup_method(self, method): ).sum() # use Int64Index, to make sure things work - self.ymd.index.set_levels( - [lev.astype("i8") for lev in self.ymd.index.levels], inplace=True + self.ymd.index = self.ymd.index.set_levels( + [lev.astype("i8") for lev in self.ymd.index.levels] ) self.ymd.index.set_names(["year", "month", "day"], inplace=True) From eabd18a6ebc6fa319ac96b88523bc7c3e813545a Mon Sep 17 00:00:00 2001 From: tp Date: Thu, 6 Aug 2020 21:24:36 +0100 Subject: [PATCH 02/10] REF: Index._validate_names --- pandas/core/indexes/base.py | 31 ++++++++++++++++++++----------- pandas/core/indexes/range.py | 5 ++--- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index bfdfbd35f27ad..1d2baa6bd3cc2 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -812,13 +812,11 @@ def copy(self, name=None, deep=False, dtype=None, names=None): In most cases, there should be no functional difference from using ``deep``, but if ``deep`` is passed it will attempt to deepcopy. """ + name = self._validate_names(name=name, names=names, deep=deep)[0] if deep: - new_index = self._shallow_copy(self._data.copy()) + new_index = self._shallow_copy(self._data.copy(), name=name) else: - new_index = self._shallow_copy() - - names = self._validate_names(name=name, names=names, deep=deep) - new_index = new_index.set_names(names) + new_index = self._shallow_copy(name=name) if dtype: new_index = new_index.astype(dtype) @@ -1186,7 +1184,7 @@ def name(self, value): maybe_extract_name(value, None, type(self)) self._name = value - def _validate_names(self, name=None, names=None, deep: bool = False): + def _validate_names(self, name=None, names=None, deep: bool = False) -> List[Label]: """ Handles the quirks of having a singular 'name' parameter for general Index and plural 'names' parameter for MultiIndex. @@ -1196,15 +1194,26 @@ def _validate_names(self, name=None, names=None, deep: bool = False): if names is not None and name is not None: raise TypeError("Can only provide one of `names` and `name`") elif names is None and name is None: - return deepcopy(self.names) if deep else self.names + 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`.") - return names + new_names = names + elif not is_list_like(name): + new_names = [name] else: - if not is_list_like(name): - return [name] - return name + new_names = name + + if len(new_names) != len(self.names): + raise ValueError( + f"Length of new names must be {len(self.names)}, got {len(new_names)}" + ) + # All items in 'new_names' need to be hashable + for new_name in new_names: + if not is_hashable(new_name): + raise TypeError(f"{type(self).__name__}.name must be a hashable type") + + return new_names def _get_names(self): return FrozenList((self.name,)) diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index e9c4c301f4dca..3577a7aacc008 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -388,9 +388,8 @@ def _shallow_copy(self, values=None, name: Label = no_default): def copy(self, name=None, deep=False, dtype=None, names=None): self._validate_dtype(dtype) - new_index = self._shallow_copy() - names = self._validate_names(name=name, names=names, deep=deep) - new_index = new_index.set_names(names) + name = self._validate_names(name=name, names=names, deep=deep)[0] + new_index = self._shallow_copy(name=name) return new_index def _minmax(self, meth: str): From 301a650e510c8a22b10486d3ae3f08bcabbad054 Mon Sep 17 00:00:00 2001 From: tp Date: Fri, 7 Aug 2020 08:58:41 +0100 Subject: [PATCH 03/10] add tests --- pandas/tests/indexes/common.py | 14 ++++++++++++++ pandas/tests/indexes/multi/test_names.py | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py index 238ee8d304d05..98f7c0eadb4bb 100644 --- a/pandas/tests/indexes/common.py +++ b/pandas/tests/indexes/common.py @@ -270,6 +270,20 @@ def test_copy_name(self, index): s3 = s1 * s2 assert s3.index.name == "mario" + def test_name2(self, index): + # gh-35592 + if isinstance(index, MultiIndex): + return + + 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"]) + + msg = f"{type(index).__name__}.name must be a hashable type" + with pytest.raises(TypeError, match=msg): + index.copy(name=[["mario"]]) + def test_ensure_copied_data(self, index): # Check the "copy" argument of each Index.__new__ is honoured # GH12309 diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index 479b5ef0211a0..6cb7b318f62ec 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -75,6 +75,12 @@ def test_copy_names(): assert multi_idx.names == ["MyName1", "MyName2"] assert multi_idx3.names == ["NewName1", "NewName2"] + with pytest.raises(ValueError, match="Length of new names must be 2, got 1"): + multi_idx.copy(names=["mario"]) + + with pytest.raises(TypeError, match="MultiIndex.name must be a hashable type"): + multi_idx.copy(names=[["mario"], ["luigi"]]) + def test_names(idx, index_names): From e161ccd4016a0a4a612d8c3ce20a16ae24b9e0af Mon Sep 17 00:00:00 2001 From: tp Date: Fri, 7 Aug 2020 09:06:15 +0100 Subject: [PATCH 04/10] add gh number --- pandas/tests/indexes/multi/test_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index 6cb7b318f62ec..f38da7ad2ae1c 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -75,6 +75,7 @@ def test_copy_names(): assert multi_idx.names == ["MyName1", "MyName2"] assert multi_idx3.names == ["NewName1", "NewName2"] + # gh-35592 with pytest.raises(ValueError, match="Length of new names must be 2, got 1"): multi_idx.copy(names=["mario"]) From 21cc85bde9fa509266ef1e6ea6e3a6c83907f702 Mon Sep 17 00:00:00 2001 From: tp Date: Fri, 7 Aug 2020 21:52:45 +0100 Subject: [PATCH 05/10] refactor to validate_all_hashable --- pandas/core/dtypes/common.py | 23 +++++++++++++++++++++++ pandas/core/indexes/base.py | 11 +++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 73109020b1b54..cd169cf573295 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1732,6 +1732,29 @@ def _validate_date_like_dtype(dtype) -> None: ) +def validate_all_hashable(*args) -> None: + """ + Return None if all args are hashable, else raise a TypeError. + + Raises + ------ + TypeError : If an argument is not hashable + + Returns + ------- + None + + Examples + -------- + >>> validate_all_hashable(1) + + >>> validate_all_hashable([1]) + ValueError: All elements must be hashable + """ + if not all(is_hashable(x) for x in args): + raise TypeError("All elements must be hashable") + + def pandas_dtype(dtype) -> DtypeObj: """ Convert input into a pandas only dtype object or a numpy dtype object. diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 1d2baa6bd3cc2..3e069dfc0db78 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -58,6 +58,7 @@ is_timedelta64_dtype, is_unsigned_integer_dtype, pandas_dtype, + validate_all_hashable, ) from pandas.core.dtypes.concat import concat_compat from pandas.core.dtypes.generic import ( @@ -1208,10 +1209,9 @@ def _validate_names(self, name=None, names=None, deep: bool = False) -> List[Lab raise ValueError( f"Length of new names must be {len(self.names)}, got {len(new_names)}" ) + # All items in 'new_names' need to be hashable - for new_name in new_names: - if not is_hashable(new_name): - raise TypeError(f"{type(self).__name__}.name must be a hashable type") + validate_all_hashable(*new_names) return new_names @@ -1241,9 +1241,8 @@ def _set_names(self, values, level=None): # GH 20527 # All items in 'name' need to be hashable: - for name in values: - if not is_hashable(name): - raise TypeError(f"{type(self).__name__}.name must be a hashable type") + validate_all_hashable(*values) + self._name = values[0] names = property(fset=_set_names, fget=_get_names) From 40b720bc5e466bc0b5f17f9d75a5424d52e10900 Mon Sep 17 00:00:00 2001 From: tp Date: Fri, 7 Aug 2020 22:44:25 +0100 Subject: [PATCH 06/10] fixes --- pandas/core/dtypes/common.py | 4 ++-- pandas/tests/indexes/multi/test_names.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index cd169cf573295..5e90b2363a7b7 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1749,9 +1749,9 @@ def validate_all_hashable(*args) -> None: >>> validate_all_hashable(1) >>> validate_all_hashable([1]) - ValueError: All elements must be hashable + TypeError: All elements must be hashable """ - if not all(is_hashable(x) for x in args): + if not all(is_hashable(arg) for arg in args): raise TypeError("All elements must be hashable") diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index f38da7ad2ae1c..4fbc6b5f520c2 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -79,7 +79,7 @@ def test_copy_names(): with pytest.raises(ValueError, match="Length of new names must be 2, got 1"): multi_idx.copy(names=["mario"]) - with pytest.raises(TypeError, match="MultiIndex.name must be a hashable type"): + with pytest.raises(TypeError, match="All elements must be hashable"): multi_idx.copy(names=[["mario"], ["luigi"]]) From 258872d819f503409c3d3b11632ff39a06d04fd1 Mon Sep 17 00:00:00 2001 From: tp Date: Fri, 7 Aug 2020 23:36:02 +0100 Subject: [PATCH 07/10] fixes --- pandas/core/dtypes/common.py | 7 +++++-- pandas/core/indexes/base.py | 4 ++-- pandas/tests/indexes/multi/test_names.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 5e90b2363a7b7..65708fea1f5b4 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1732,7 +1732,7 @@ def _validate_date_like_dtype(dtype) -> None: ) -def validate_all_hashable(*args) -> None: +def validate_all_hashable(*args, name: str = None) -> None: """ Return None if all args are hashable, else raise a TypeError. @@ -1752,7 +1752,10 @@ def validate_all_hashable(*args) -> None: TypeError: All elements must be hashable """ if not all(is_hashable(arg) for arg in args): - raise TypeError("All elements must be hashable") + if name: + raise TypeError(f"{name} must be a hashable type") + else: + raise TypeError("All elements must be hashable") def pandas_dtype(dtype) -> DtypeObj: diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 3e069dfc0db78..abf8dc8e70c83 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1211,7 +1211,7 @@ def _validate_names(self, name=None, names=None, deep: bool = False) -> List[Lab ) # All items in 'new_names' need to be hashable - validate_all_hashable(*new_names) + validate_all_hashable(*new_names, name=f"{type(self).__name__}.name") return new_names @@ -1241,7 +1241,7 @@ def _set_names(self, values, level=None): # GH 20527 # All items in 'name' need to be hashable: - validate_all_hashable(*values) + validate_all_hashable(*values, name=f"{type(self).__name__}.name") self._name = values[0] diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index 4fbc6b5f520c2..f38da7ad2ae1c 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -79,7 +79,7 @@ def test_copy_names(): with pytest.raises(ValueError, match="Length of new names must be 2, got 1"): multi_idx.copy(names=["mario"]) - with pytest.raises(TypeError, match="All elements must be hashable"): + with pytest.raises(TypeError, match="MultiIndex.name must be a hashable type"): multi_idx.copy(names=[["mario"], ["luigi"]]) From 77c28a51013d10ce722c4f41cb7a4ecbd1fc9365 Mon Sep 17 00:00:00 2001 From: tp Date: Sat, 8 Aug 2020 00:18:22 +0100 Subject: [PATCH 08/10] more fixes --- pandas/core/dtypes/common.py | 20 ++++++++++---------- pandas/core/indexes/base.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 65708fea1f5b4..c17a51d7d623d 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -1732,10 +1732,17 @@ def _validate_date_like_dtype(dtype) -> None: ) -def validate_all_hashable(*args, name: str = None) -> None: +def validate_all_hashable(*args, error_name=None) -> None: """ Return None if all args are hashable, else raise a TypeError. + Parameters + ---------- + *args + Arguments to validate. + error_name : str, optional + The name to use if error + Raises ------ TypeError : If an argument is not hashable @@ -1743,17 +1750,10 @@ def validate_all_hashable(*args, name: str = None) -> None: Returns ------- None - - Examples - -------- - >>> validate_all_hashable(1) - - >>> validate_all_hashable([1]) - TypeError: All elements must be hashable """ if not all(is_hashable(arg) for arg in args): - if name: - raise TypeError(f"{name} must be a hashable type") + if error_name: + raise TypeError(f"{error_name} must be a hashable type") else: raise TypeError("All elements must be hashable") diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index abf8dc8e70c83..38ac869276ba4 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1211,7 +1211,7 @@ def _validate_names(self, name=None, names=None, deep: bool = False) -> List[Lab ) # All items in 'new_names' need to be hashable - validate_all_hashable(*new_names, name=f"{type(self).__name__}.name") + validate_all_hashable(*new_names, error_name=f"{type(self).__name__}.name") return new_names @@ -1241,7 +1241,7 @@ def _set_names(self, values, level=None): # GH 20527 # All items in 'name' need to be hashable: - validate_all_hashable(*values, name=f"{type(self).__name__}.name") + validate_all_hashable(*values, error_name=f"{type(self).__name__}.name") self._name = values[0] From 430d0cc2a683451cafd1628f3102c48273ca302d Mon Sep 17 00:00:00 2001 From: tp Date: Sat, 8 Aug 2020 12:48:48 +0100 Subject: [PATCH 09/10] add test for validate_all_hashable --- pandas/core/dtypes/common.py | 4 ++-- pandas/tests/dtypes/test_common.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index c17a51d7d623d..1e70ff90fcd44 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -9,7 +9,7 @@ from pandas._libs import Interval, Period, algos from pandas._libs.tslibs import conversion -from pandas._typing import ArrayLike, DtypeObj +from pandas._typing import ArrayLike, DtypeObj, Optional from pandas.core.dtypes.base import registry from pandas.core.dtypes.dtypes import ( @@ -1732,7 +1732,7 @@ def _validate_date_like_dtype(dtype) -> None: ) -def validate_all_hashable(*args, error_name=None) -> None: +def validate_all_hashable(*args, error_name: Optional[str] = None) -> None: """ Return None if all args are hashable, else raise a TypeError. diff --git a/pandas/tests/dtypes/test_common.py b/pandas/tests/dtypes/test_common.py index ce12718e48d0d..a6c526fcb008a 100644 --- a/pandas/tests/dtypes/test_common.py +++ b/pandas/tests/dtypes/test_common.py @@ -746,3 +746,13 @@ def test_astype_object_preserves_datetime_na(from_type): result = astype_nansafe(arr, dtype="object") assert isna(result)[0] + + +def test_validate_allhashable(): + assert com.validate_all_hashable(1, "a") is None + + with pytest.raises(TypeError, match="All elements must be hashable"): + com.validate_all_hashable([]) + + with pytest.raises(TypeError, match="list must be a hashable type"): + com.validate_all_hashable([], error_name="list") From c78d99e8db6dc001bfbb305caf0f298a8e6b3e96 Mon Sep 17 00:00:00 2001 From: tp Date: Sat, 8 Aug 2020 16:33:35 +0100 Subject: [PATCH 10/10] use validate_all_hashable in Series.name --- pandas/core/series.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index 9e70120f67969..93368ea1e515f 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -54,6 +54,7 @@ is_list_like, is_object_dtype, is_scalar, + validate_all_hashable, ) from pandas.core.dtypes.generic import ABCDataFrame from pandas.core.dtypes.inference import is_hashable @@ -491,8 +492,7 @@ def name(self) -> Label: @name.setter def name(self, value: Label) -> None: - if not is_hashable(value): - raise TypeError("Series.name must be a hashable type") + validate_all_hashable(value, error_name=f"{type(self).__name__}.name") object.__setattr__(self, "_name", value) @property