Skip to content

TYP: Series/DataFrame/Index are not Hashable #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 8, 2022
2 changes: 1 addition & 1 deletion pandas-stubs/_typing.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,5 @@ XMLParsers = Literal["lxml", "etree"]
# Any plain Python or numpy function
Function = Union[np.ufunc, Callable[..., Any]]
GroupByObject = Union[
Label, List[Label], Function, Series, np.ndarray, Mapping[Label, Any]
Label, List[Label], Function, Series, np.ndarray, Mapping[Label, Any], Index
]
9 changes: 6 additions & 3 deletions pandas-stubs/core/frame.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import datetime as _dt
from typing import (
Any,
Callable,
ClassVar,
Dict,
Hashable,
Iterable,
Expand Down Expand Up @@ -183,6 +184,8 @@ class DataFrame(NDFrame, OpsMixin):
Index,
Series,
]
__hash__: ClassVar[None] # type: ignore[assignment]

def __new__(
cls,
data: Optional[Union[_ListLike, DataFrame, Dict[Any, Any]]] = ...,
Expand Down Expand Up @@ -428,7 +431,7 @@ class DataFrame(NDFrame, OpsMixin):
*,
axis: Axis = ...,
index: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] | Index = ...,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Index is now Hashable, do you need to change this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index was declared to be Hashable but it isn't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index probably needs to be added to many more annotations. Just added it here because a test was falling without it.

level: Optional[Level] = ...,
inplace: Literal[True],
errors: IgnoreRaise = ...,
Expand All @@ -440,7 +443,7 @@ class DataFrame(NDFrame, OpsMixin):
*,
axis: Axis = ...,
index: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] | Index = ...,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above. If Index is now Hashable, do you need to change this?

level: Optional[Level] = ...,
inplace: Literal[False] = ...,
errors: IgnoreRaise = ...,
Expand All @@ -452,7 +455,7 @@ class DataFrame(NDFrame, OpsMixin):
*,
axis: Axis = ...,
index: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] = ...,
columns: Hashable | Sequence[Hashable] | Index = ...,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above. If Index is now Hashable, do you need to change this?

level: Optional[Level] = ...,
inplace: bool = ...,
errors: IgnoreRaise = ...,
Expand Down
3 changes: 2 additions & 1 deletion pandas-stubs/core/generic.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sys
from typing import (
Any,
Callable,
ClassVar,
Dict,
Hashable,
Iterator,
Expand Down Expand Up @@ -89,7 +90,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
def bool(self) -> _bool: ...
def __abs__(self) -> NDFrame: ...
def __round__(self, decimals: int = ...) -> NDFrame: ...
def __hash__(self): ...
__hash__: ClassVar[None] # type: ignore[assignment]
def __iter__(self) -> Iterator: ...
def keys(self): ...
def iteritems(self): ...
Expand Down
3 changes: 2 additions & 1 deletion pandas-stubs/core/indexes/base.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import (
Callable,
ClassVar,
Dict,
Hashable,
Iterable,
Expand Down Expand Up @@ -169,7 +170,7 @@ class Index(IndexOpsMixin, PandasObject):
def where(self, cond, other=...): ...
def is_type_compatible(self, kind) -> bool: ...
def __contains__(self, key) -> bool: ...
def __hash__(self) -> int: ...
__hash__: ClassVar[None] # type: ignore[assignment]
def __setitem__(self, key, value) -> None: ...
@overload
def __getitem__(
Expand Down
2 changes: 1 addition & 1 deletion pandas-stubs/core/indexes/frozen.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ class FrozenList(PandasObject, list):
def __eq__(self, other) -> bool: ...
def __mul__(self, other): ...
def __reduce__(self): ...
def __hash__(self): ...
def __hash__(self) -> int: ... # type: ignore[override]
3 changes: 3 additions & 0 deletions pandas-stubs/core/series.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from datetime import (
from typing import (
Any,
Callable,
ClassVar,
Dict,
Generic,
Hashable,
Expand Down Expand Up @@ -138,6 +139,8 @@ class _LocIndexerSeries(_LocIndexer, Generic[S1]):
class Series(IndexOpsMixin, NDFrame, Generic[S1]):

_ListLike = Union[ArrayLike, Dict[_str, np.ndarray], List, Tuple, Index]
__hash__: ClassVar[None]

@overload
def __new__(
cls,
Expand Down
27 changes: 27 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import typing_extensions
import types


def assert_type(actual_object, expected):
# rough check whether the types might match

actual = type(actual_object).__name__.split(".")[-1]
error = False

if isinstance(expected, types.GenericAlias): # type: ignore[attr-defined]
expected = expected.__name__

if isinstance(expected, str):
actual = actual.lower()
expected = expected.lower()
error = actual not in expected
elif expected is None:
error = actual_object is not None
else:
error = not isinstance(actual_object, expected)

if error:
raise TypeError(f"Expected '{expected}' got '{actual}'")


typing_extensions.assert_type = assert_type
10 changes: 10 additions & 0 deletions tests/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,3 +1141,13 @@ def test_frame_ndarray_assignmment() -> None:

df_b = pd.DataFrame({"a": [0.0] * 10, "b": [1.0] * 10})
df_b.iloc[:, :] = np.array([[-1.0, np.inf]] * 10)


def test_not_hashable() -> None:
# GH 113
assert_type(pd.DataFrame.__hash__, None)
assert_type(pd.DataFrame().__hash__, None)
assert_type(pd.Series.__hash__, None)
assert_type(pd.Series([], dtype=object).__hash__, None)
assert_type(pd.Index.__hash__, None)
assert_type(pd.Index([]).__hash__, None)
4 changes: 2 additions & 2 deletions tests/test_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ def test_index_unique() -> None:

df = pd.DataFrame({"x": [1, 2, 3, 4]}, index=pd.Index([1, 2, 3, 2]))
ind = df.index
assert_type(ind, "pd.Index")
assert_type(ind, pd.Index)
i2 = ind.unique()
assert_type(i2, "pd.Index")
assert_type(i2, pd.Index)


def test_index_isin() -> None:
Expand Down