Skip to content

Commit 6c1597e

Browse files
jbrockmendeljreback
authored andcommitted
REF: implement indexes.extension to share delegation (#30629)
1 parent 1e32421 commit 6c1597e

File tree

5 files changed

+124
-125
lines changed

5 files changed

+124
-125
lines changed

pandas/core/indexes/datetimelike.py

+17-62
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Base and utility classes for tseries type pandas objects.
33
"""
44
import operator
5-
from typing import List, Set
5+
from typing import List, Optional, Set
66

77
import numpy as np
88

@@ -40,28 +40,9 @@
4040

4141
from pandas.tseries.frequencies import DateOffset, to_offset
4242

43-
_index_doc_kwargs = dict(ibase._index_doc_kwargs)
44-
43+
from .extension import inherit_names
4544

46-
def ea_passthrough(array_method):
47-
"""
48-
Make an alias for a method of the underlying ExtensionArray.
49-
50-
Parameters
51-
----------
52-
array_method : method on an Array class
53-
54-
Returns
55-
-------
56-
method
57-
"""
58-
59-
def method(self, *args, **kwargs):
60-
return array_method(self._data, *args, **kwargs)
61-
62-
method.__name__ = array_method.__name__
63-
method.__doc__ = array_method.__doc__
64-
return method
45+
_index_doc_kwargs = dict(ibase._index_doc_kwargs)
6546

6647

6748
def _make_wrapped_arith_op(opname):
@@ -100,48 +81,34 @@ def wrapper(left, right):
10081
return wrapper
10182

10283

84+
@inherit_names(
85+
["inferred_freq", "_isnan", "_resolution", "resolution"],
86+
DatetimeLikeArrayMixin,
87+
cache=True,
88+
)
89+
@inherit_names(
90+
["__iter__", "mean", "freq", "freqstr", "_ndarray_values", "asi8", "_box_values"],
91+
DatetimeLikeArrayMixin,
92+
)
10393
class DatetimeIndexOpsMixin(ExtensionOpsMixin):
10494
"""
10595
Common ops mixin to support a unified interface datetimelike Index.
10696
"""
10797

10898
_data: ExtensionArray
99+
freq: Optional[DateOffset]
100+
freqstr: Optional[str]
101+
_resolution: int
102+
_bool_ops: List[str] = []
103+
_field_ops: List[str] = []
109104

110-
# DatetimeLikeArrayMixin assumes subclasses are mutable, so these are
111-
# properties there. They can be made into cache_readonly for Index
112-
# subclasses bc they are immutable
113-
inferred_freq = cache_readonly(
114-
DatetimeLikeArrayMixin.inferred_freq.fget # type: ignore
115-
)
116-
_isnan = cache_readonly(DatetimeLikeArrayMixin._isnan.fget) # type: ignore
117105
hasnans = cache_readonly(DatetimeLikeArrayMixin._hasnans.fget) # type: ignore
118106
_hasnans = hasnans # for index / array -agnostic code
119-
_resolution = cache_readonly(
120-
DatetimeLikeArrayMixin._resolution.fget # type: ignore
121-
)
122-
resolution = cache_readonly(DatetimeLikeArrayMixin.resolution.fget) # type: ignore
123-
124-
__iter__ = ea_passthrough(DatetimeLikeArrayMixin.__iter__)
125-
mean = ea_passthrough(DatetimeLikeArrayMixin.mean)
126107

127108
@property
128109
def is_all_dates(self) -> bool:
129110
return True
130111

131-
@property
132-
def freq(self):
133-
"""
134-
Return the frequency object if it is set, otherwise None.
135-
"""
136-
return self._data.freq
137-
138-
@property
139-
def freqstr(self):
140-
"""
141-
Return the frequency object as a string if it is set, otherwise None.
142-
"""
143-
return self._data.freqstr
144-
145112
def unique(self, level=None):
146113
if level is not None:
147114
self._validate_index_level(level)
@@ -172,10 +139,6 @@ def wrapper(self, other):
172139
wrapper.__name__ = f"__{op.__name__}__"
173140
return wrapper
174141

175-
@property
176-
def _ndarray_values(self) -> np.ndarray:
177-
return self._data._ndarray_values
178-
179142
# ------------------------------------------------------------------------
180143
# Abstract data attributes
181144

@@ -184,11 +147,6 @@ def values(self):
184147
# Note: PeriodArray overrides this to return an ndarray of objects.
185148
return self._data._data
186149

187-
@property # type: ignore # https://github.com/python/mypy/issues/1362
188-
@Appender(DatetimeLikeArrayMixin.asi8.__doc__)
189-
def asi8(self):
190-
return self._data.asi8
191-
192150
def __array_wrap__(self, result, context=None):
193151
"""
194152
Gets called after a ufunc.
@@ -248,9 +206,6 @@ def _ensure_localized(
248206
return type(self)._simple_new(result, name=self.name)
249207
return arg
250208

251-
def _box_values(self, values):
252-
return self._data._box_values(values)
253-
254209
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
255210
def __contains__(self, key):
256211
try:

pandas/core/indexes/datetimes.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
DatetimelikeDelegateMixin,
3636
DatetimeTimedeltaMixin,
3737
)
38+
from pandas.core.indexes.extension import inherit_names
3839
from pandas.core.ops import get_op_result_name
3940
import pandas.core.tools.datetimes as tools
4041

@@ -72,6 +73,7 @@ class DatetimeDelegateMixin(DatetimelikeDelegateMixin):
7273
"_local_timestamps",
7374
"_has_same_tz",
7475
"_format_native_types",
76+
"__iter__",
7577
]
7678
_extra_raw_properties = ["_box_func", "tz", "tzinfo", "dtype"]
7779
_delegated_properties = DatetimeArray._datetimelike_ops + _extra_raw_properties
@@ -87,6 +89,17 @@ class DatetimeDelegateMixin(DatetimelikeDelegateMixin):
8789
_delegate_class = DatetimeArray
8890

8991

92+
@inherit_names(["_timezone", "is_normalized", "_resolution"], DatetimeArray, cache=True)
93+
@inherit_names(
94+
[
95+
"_bool_ops",
96+
"_object_ops",
97+
"_field_ops",
98+
"_datetimelike_ops",
99+
"_datetimelike_methods",
100+
],
101+
DatetimeArray,
102+
)
90103
@delegate_names(
91104
DatetimeArray, DatetimeDelegateMixin._delegated_properties, typ="property"
92105
)
@@ -209,15 +222,6 @@ class DatetimeIndex(DatetimeTimedeltaMixin, DatetimeDelegateMixin):
209222
_is_numeric_dtype = False
210223
_infer_as_myclass = True
211224

212-
# Use faster implementation given we know we have DatetimeArrays
213-
__iter__ = DatetimeArray.__iter__
214-
# some things like freq inference make use of these attributes.
215-
_bool_ops = DatetimeArray._bool_ops
216-
_object_ops = DatetimeArray._object_ops
217-
_field_ops = DatetimeArray._field_ops
218-
_datetimelike_ops = DatetimeArray._datetimelike_ops
219-
_datetimelike_methods = DatetimeArray._datetimelike_methods
220-
221225
tz: Optional[tzinfo]
222226

223227
# --------------------------------------------------------------------
@@ -947,10 +951,6 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None):
947951
# --------------------------------------------------------------------
948952
# Wrapping DatetimeArray
949953

950-
_timezone = cache_readonly(DatetimeArray._timezone.fget) # type: ignore
951-
is_normalized = cache_readonly(DatetimeArray.is_normalized.fget) # type: ignore
952-
_resolution = cache_readonly(DatetimeArray._resolution.fget) # type: ignore
953-
954954
def __getitem__(self, key):
955955
result = self._data.__getitem__(key)
956956
if is_scalar(result):

pandas/core/indexes/extension.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Shared methods for Index subclasses backed by ExtensionArray.
3+
"""
4+
from typing import List
5+
6+
from pandas.util._decorators import cache_readonly
7+
8+
9+
def inherit_from_data(name: str, delegate, cache: bool = False):
10+
"""
11+
Make an alias for a method of the underlying ExtensionArray.
12+
13+
Parameters
14+
----------
15+
name : str
16+
Name of an attribute the class should inherit from its EA parent.
17+
delegate : class
18+
cache : bool, default False
19+
Whether to convert wrapped properties into cache_readonly
20+
21+
Returns
22+
-------
23+
attribute, method, property, or cache_readonly
24+
"""
25+
26+
attr = getattr(delegate, name)
27+
28+
if isinstance(attr, property):
29+
if cache:
30+
method = cache_readonly(attr.fget)
31+
32+
else:
33+
34+
def fget(self):
35+
return getattr(self._data, name)
36+
37+
def fset(self, value):
38+
setattr(self._data, name, value)
39+
40+
fget.__name__ = name
41+
fget.__doc__ = attr.__doc__
42+
43+
method = property(fget, fset)
44+
45+
elif not callable(attr):
46+
# just a normal attribute, no wrapping
47+
method = attr
48+
49+
else:
50+
51+
def method(self, *args, **kwargs):
52+
result = attr(self._data, *args, **kwargs)
53+
return result
54+
55+
method.__name__ = name
56+
method.__doc__ = attr.__doc__
57+
return method
58+
59+
60+
def inherit_names(names: List[str], delegate, cache: bool = False):
61+
"""
62+
Class decorator to pin attributes from an ExtensionArray to a Index subclass.
63+
64+
Parameters
65+
----------
66+
names : List[str]
67+
delegate : class
68+
cache : bool, default False
69+
"""
70+
71+
def wrapper(cls):
72+
for name in names:
73+
meth = inherit_from_data(name, delegate, cache=cache)
74+
setattr(cls, name, meth)
75+
76+
return cls
77+
78+
return wrapper

pandas/core/indexes/interval.py

+4-41
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
from pandas.tseries.frequencies import to_offset
5959
from pandas.tseries.offsets import DateOffset
6060

61+
from .extension import inherit_names
62+
6163
_VALID_CLOSED = {"left", "right", "both", "neither"}
6264
_index_doc_kwargs = dict(ibase._index_doc_kwargs)
6365

@@ -199,10 +201,11 @@ def func(intvidx_self, other, sort=False):
199201
)
200202
@accessor.delegate_names(
201203
delegate=IntervalArray,
202-
accessors=["__array__", "overlaps", "contains"],
204+
accessors=["__array__", "overlaps", "contains", "__len__", "set_closed"],
203205
typ="method",
204206
overwrite=True,
205207
)
208+
@inherit_names(["is_non_overlapping_monotonic", "mid"], IntervalArray, cache=True)
206209
class IntervalIndex(IntervalMixin, Index, accessor.PandasDelegate):
207210
_typ = "intervalindex"
208211
_comparables = ["name"]
@@ -412,34 +415,6 @@ def to_tuples(self, na_tuple=True):
412415
def _multiindex(self):
413416
return MultiIndex.from_arrays([self.left, self.right], names=["left", "right"])
414417

415-
@Appender(
416-
_interval_shared_docs["set_closed"]
417-
% dict(
418-
klass="IntervalIndex",
419-
examples=textwrap.dedent(
420-
"""\
421-
Examples
422-
--------
423-
>>> index = pd.interval_range(0, 3)
424-
>>> index
425-
IntervalIndex([(0, 1], (1, 2], (2, 3]],
426-
closed='right',
427-
dtype='interval[int64]')
428-
>>> index.set_closed('both')
429-
IntervalIndex([[0, 1], [1, 2], [2, 3]],
430-
closed='both',
431-
dtype='interval[int64]')
432-
"""
433-
),
434-
)
435-
)
436-
def set_closed(self, closed):
437-
array = self._data.set_closed(closed)
438-
return self._simple_new(array, self.name) # TODO: can we use _shallow_copy?
439-
440-
def __len__(self) -> int:
441-
return len(self.left)
442-
443418
@cache_readonly
444419
def values(self):
445420
"""
@@ -479,13 +454,6 @@ def memory_usage(self, deep: bool = False) -> int:
479454
# so return the bytes here
480455
return self.left.memory_usage(deep=deep) + self.right.memory_usage(deep=deep)
481456

482-
@cache_readonly
483-
def mid(self):
484-
"""
485-
Return the midpoint of each Interval in the IntervalIndex as an Index.
486-
"""
487-
return self._data.mid
488-
489457
@cache_readonly
490458
def is_monotonic(self) -> bool:
491459
"""
@@ -534,11 +502,6 @@ def is_unique(self):
534502

535503
return True
536504

537-
@cache_readonly
538-
@Appender(_interval_shared_docs["is_non_overlapping_monotonic"] % _index_doc_kwargs)
539-
def is_non_overlapping_monotonic(self):
540-
return self._data.is_non_overlapping_monotonic
541-
542505
@property
543506
def is_overlapping(self):
544507
"""

0 commit comments

Comments
 (0)