Skip to content

Commit ef4ad02

Browse files
authored
TYP: IntervalIndex.left (#1549)
* #1548 (comment) * mypy * tests
1 parent d54943e commit ef4ad02

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

pandas-stubs/_libs/interval.pyi

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ from typing import (
77
type_check_only,
88
)
99

10+
import numpy as np
1011
from pandas import (
1112
IntervalIndex,
1213
Series,
@@ -65,9 +66,9 @@ class IntervalMixin:
6566

6667
class Interval(IntervalMixin, Generic[_OrderableT]):
6768
@property
68-
def left(self: Interval[_OrderableT]) -> _OrderableT: ...
69+
def left(self) -> _OrderableT: ...
6970
@property
70-
def right(self: Interval[_OrderableT]) -> _OrderableT: ...
71+
def right(self) -> _OrderableT: ...
7172
@property
7273
def closed(self) -> IntervalClosedType: ...
7374
mid = _MidDescriptor()
@@ -79,16 +80,10 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
7980
closed: IntervalClosedType = ...,
8081
) -> None: ...
8182
def __hash__(self) -> int: ...
82-
# for __contains__, it seems that we have to separate out the 4 cases to make
83-
# mypy happy
8483
@overload
85-
def __contains__(self: Interval[Timestamp], key: Timestamp) -> bool: ...
84+
def __contains__(self: Interval[int], key: float | np.floating) -> bool: ...
8685
@overload
87-
def __contains__(self: Interval[Timedelta], key: Timedelta) -> bool: ...
88-
@overload
89-
def __contains__(self: Interval[int], key: float) -> bool: ...
90-
@overload
91-
def __contains__(self: Interval[float], key: float) -> bool: ...
86+
def __contains__(self, key: _OrderableT) -> bool: ...
9287
@overload
9388
def __add__(self: Interval[Timestamp], y: Timedelta) -> Interval[Timestamp]: ...
9489
@overload

pandas-stubs/core/indexes/interval.pyi

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ from typing import (
77
Literal,
88
TypeAlias,
99
overload,
10+
type_check_only,
1011
)
1112

1213
import numpy as np
@@ -17,7 +18,11 @@ from pandas.core.indexes.extension import ExtensionIndex
1718
from pandas._libs.interval import (
1819
Interval as Interval,
1920
IntervalMixin,
21+
_OrderableScalarT,
22+
_OrderableT,
23+
_OrderableTimesT,
2024
)
25+
from pandas._libs.tslibs.timedeltas import Timedelta
2126
from pandas._typing import (
2227
DatetimeLike,
2328
DtypeArg,
@@ -58,6 +63,36 @@ _EdgesTimedelta: TypeAlias = (
5863
_TimestampLike: TypeAlias = pd.Timestamp | np.datetime64 | dt.datetime
5964
_TimedeltaLike: TypeAlias = pd.Timedelta | np.timedelta64 | dt.timedelta
6065

66+
@type_check_only
67+
class _LengthDescriptor:
68+
@overload
69+
def __get__(
70+
self,
71+
instance: IntervalIndex[Interval[_OrderableScalarT]],
72+
owner: type[IntervalIndex],
73+
) -> Index[_OrderableScalarT]: ...
74+
@overload
75+
def __get__(
76+
self,
77+
instance: IntervalIndex[Interval[_OrderableTimesT]],
78+
owner: type[IntervalIndex],
79+
) -> Index[Timedelta]: ...
80+
81+
@type_check_only
82+
class _MidDescriptor:
83+
@overload
84+
def __get__(
85+
self,
86+
instance: IntervalIndex[Interval[int]],
87+
owner: type[IntervalIndex],
88+
) -> Index[float]: ...
89+
@overload
90+
def __get__(
91+
self,
92+
instance: IntervalIndex[Interval[_OrderableT]],
93+
owner: type[IntervalIndex],
94+
) -> Index[_OrderableT]: ...
95+
6196
class IntervalIndex(ExtensionIndex[IntervalT, np.object_], IntervalMixin):
6297
closed: IntervalClosedType
6398

@@ -216,16 +251,13 @@ class IntervalIndex(ExtensionIndex[IntervalT, np.object_], IntervalMixin):
216251
def is_overlapping(self) -> bool: ...
217252
def get_loc(self, key: Label) -> int | slice | np_1darray_bool: ...
218253
@property
219-
def left(self) -> Index: ...
220-
@property
221-
def right(self) -> Index: ...
222-
@property
223-
def mid(self) -> Index: ...
254+
def left(self: IntervalIndex[Interval[_OrderableT]]) -> Index[_OrderableT]: ...
224255
@property
225-
def length(self) -> Index: ...
256+
def right(self: IntervalIndex[Interval[_OrderableT]]) -> Index[_OrderableT]: ...
257+
mid = _MidDescriptor()
258+
length = _LengthDescriptor()
226259
@overload # type: ignore[override]
227-
# pyrefly: ignore # bad-override
228-
def __getitem__(
260+
def __getitem__( # pyrefly: ignore[bad-override]
229261
self,
230262
idx: (
231263
slice

tests/indexes/test_indexes.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import datetime as dt
55
import sys
66
from typing import (
7+
TYPE_CHECKING,
78
Any,
89
cast,
910
)
@@ -18,6 +19,7 @@
1819
from pandas.core.indexes.base import Index
1920
from pandas.core.indexes.category import CategoricalIndex
2021
from pandas.core.indexes.datetimes import DatetimeIndex
22+
import pytest
2123
from typing_extensions import (
2224
Never,
2325
assert_type,
@@ -847,6 +849,49 @@ def test_interval_index_tuples() -> None:
847849
)
848850

849851

852+
dt_l, dt_r = dt.datetime(2025, 12, 14), dt.datetime(2025, 12, 15)
853+
td_l, td_r = dt.timedelta(seconds=1), dt.timedelta(seconds=2)
854+
855+
856+
@pytest.mark.parametrize(
857+
("itv_idx", "typ_left", "typ_mid", "typ_length"),
858+
[
859+
(pd.interval_range(0, 10), np.integer, np.floating, np.integer),
860+
(pd.interval_range(0.0, 10), np.floating, np.floating, np.floating),
861+
(pd.interval_range(dt_l, dt_r), pd.Timestamp, pd.Timestamp, pd.Timedelta),
862+
(pd.interval_range(td_l, td_r, 2), pd.Timedelta, pd.Timedelta, pd.Timedelta),
863+
],
864+
)
865+
def test_interval_properties(
866+
itv_idx: pd.IntervalIndex[Any], typ_left: type, typ_mid: type, typ_length: type
867+
) -> None:
868+
check(itv_idx.left, pd.Index, typ_left)
869+
check(itv_idx.right, pd.Index, typ_left)
870+
check(itv_idx.mid, pd.Index, typ_mid)
871+
check(itv_idx.length, pd.Index, typ_length)
872+
873+
if TYPE_CHECKING:
874+
assert_type(pd.interval_range(0, 10).left, "pd.Index[int]")
875+
assert_type(pd.interval_range(0, 10).right, "pd.Index[int]")
876+
assert_type(pd.interval_range(0, 10).mid, "pd.Index[float]")
877+
assert_type(pd.interval_range(0, 10).length, "pd.Index[int]")
878+
879+
assert_type(pd.interval_range(0.0, 10).left, "pd.Index[float]")
880+
assert_type(pd.interval_range(0.0, 10).right, "pd.Index[float]")
881+
assert_type(pd.interval_range(0.0, 10).mid, "pd.Index[float]")
882+
assert_type(pd.interval_range(0.0, 10).length, "pd.Index[float]")
883+
884+
assert_type(pd.interval_range(dt_l, dt_r).left, "pd.Index[pd.Timestamp]")
885+
assert_type(pd.interval_range(dt_l, dt_r).right, "pd.Index[pd.Timestamp]")
886+
assert_type(pd.interval_range(dt_l, dt_r).mid, "pd.Index[pd.Timestamp]")
887+
assert_type(pd.interval_range(dt_l, dt_r).length, "pd.Index[pd.Timedelta]")
888+
889+
assert_type(pd.interval_range(td_l, td_r).left, "pd.Index[pd.Timedelta]")
890+
assert_type(pd.interval_range(td_l, td_r).right, "pd.Index[pd.Timedelta]")
891+
assert_type(pd.interval_range(td_l, td_r).mid, "pd.Index[pd.Timedelta]")
892+
assert_type(pd.interval_range(td_l, td_r, 2).length, "pd.Index[pd.Timedelta]")
893+
894+
850895
def test_sorted_and_list() -> None:
851896
# GH 497
852897
i1 = pd.Index([3, 2, 1])

0 commit comments

Comments
 (0)