1"""
2Base class for the internal managers. Both BlockManager and ArrayManager
3inherit from this class.
4"""
5from __future__ import annotations
6
7from typing import (
8 Literal,
9 TypeVar,
10 final,
11)
12
13import numpy as np
14
15from pandas._typing import (
16 ArrayLike,
17 AxisInt,
18 DtypeObj,
19 Shape,
20)
21from pandas.errors import AbstractMethodError
22
23from pandas.core.dtypes.cast import (
24 find_common_type,
25 np_can_hold_element,
26)
27
28from pandas.core.base import PandasObject
29from pandas.core.indexes.api import (
30 Index,
31 default_index,
32)
33
34T = TypeVar("T", bound="DataManager")
35
36
37class DataManager(PandasObject):
38 # TODO share more methods/attributes
39
40 axes: list[Index]
41
42 @property
43 def items(self) -> Index:
44 raise AbstractMethodError(self)
45
46 @final
47 def __len__(self) -> int:
48 return len(self.items)
49
50 @property
51 def ndim(self) -> int:
52 return len(self.axes)
53
54 @property
55 def shape(self) -> Shape:
56 return tuple(len(ax) for ax in self.axes)
57
58 @final
59 def _validate_set_axis(self, axis: AxisInt, new_labels: Index) -> None:
60 # Caller is responsible for ensuring we have an Index object.
61 old_len = len(self.axes[axis])
62 new_len = len(new_labels)
63
64 if axis == 1 and len(self.items) == 0:
65 # If we are setting the index on a DataFrame with no columns,
66 # it is OK to change the length.
67 pass
68
69 elif new_len != old_len:
70 raise ValueError(
71 f"Length mismatch: Expected axis has {old_len} elements, new "
72 f"values have {new_len} elements"
73 )
74
75 def reindex_indexer(
76 self: T,
77 new_axis,
78 indexer,
79 axis: AxisInt,
80 fill_value=None,
81 allow_dups: bool = False,
82 copy: bool = True,
83 only_slice: bool = False,
84 ) -> T:
85 raise AbstractMethodError(self)
86
87 @final
88 def reindex_axis(
89 self: T,
90 new_index: Index,
91 axis: AxisInt,
92 fill_value=None,
93 only_slice: bool = False,
94 ) -> T:
95 """
96 Conform data manager to new index.
97 """
98 new_index, indexer = self.axes[axis].reindex(new_index)
99
100 return self.reindex_indexer(
101 new_index,
102 indexer,
103 axis=axis,
104 fill_value=fill_value,
105 copy=False,
106 only_slice=only_slice,
107 )
108
109 def _equal_values(self: T, other: T) -> bool:
110 """
111 To be implemented by the subclasses. Only check the column values
112 assuming shape and indexes have already been checked.
113 """
114 raise AbstractMethodError(self)
115
116 @final
117 def equals(self, other: object) -> bool:
118 """
119 Implementation for DataFrame.equals
120 """
121 if not isinstance(other, DataManager):
122 return False
123
124 self_axes, other_axes = self.axes, other.axes
125 if len(self_axes) != len(other_axes):
126 return False
127 if not all(ax1.equals(ax2) for ax1, ax2 in zip(self_axes, other_axes)):
128 return False
129
130 return self._equal_values(other)
131
132 def apply(
133 self: T,
134 f,
135 align_keys: list[str] | None = None,
136 **kwargs,
137 ) -> T:
138 raise AbstractMethodError(self)
139
140 @final
141 def isna(self: T, func) -> T:
142 return self.apply("apply", func=func)
143
144 # --------------------------------------------------------------------
145 # Consolidation: No-ops for all but BlockManager
146
147 def is_consolidated(self) -> bool:
148 return True
149
150 def consolidate(self: T) -> T:
151 return self
152
153 def _consolidate_inplace(self) -> None:
154 return
155
156
157class SingleDataManager(DataManager):
158 @property
159 def ndim(self) -> Literal[1]:
160 return 1
161
162 @final
163 @property
164 def array(self) -> ArrayLike:
165 """
166 Quick access to the backing array of the Block or SingleArrayManager.
167 """
168 # error: "SingleDataManager" has no attribute "arrays"; maybe "array"
169 return self.arrays[0] # type: ignore[attr-defined]
170
171 def setitem_inplace(self, indexer, value) -> None:
172 """
173 Set values with indexer.
174
175 For Single[Block/Array]Manager, this backs s[indexer] = value
176
177 This is an inplace version of `setitem()`, mutating the manager/values
178 in place, not returning a new Manager (and Block), and thus never changing
179 the dtype.
180 """
181 arr = self.array
182
183 # EAs will do this validation in their own __setitem__ methods.
184 if isinstance(arr, np.ndarray):
185 # Note: checking for ndarray instead of np.dtype means we exclude
186 # dt64/td64, which do their own validation.
187 value = np_can_hold_element(arr.dtype, value)
188
189 if isinstance(value, np.ndarray) and value.ndim == 1 and len(value) == 1:
190 # NumPy 1.25 deprecation: https://github.com/numpy/numpy/pull/10615
191 value = value[0, ...]
192
193 arr[indexer] = value
194
195 def grouped_reduce(self, func):
196 arr = self.array
197 res = func(arr)
198 index = default_index(len(res))
199
200 mgr = type(self).from_array(res, index)
201 return mgr
202
203 @classmethod
204 def from_array(cls, arr: ArrayLike, index: Index):
205 raise AbstractMethodError(cls)
206
207
208def interleaved_dtype(dtypes: list[DtypeObj]) -> DtypeObj | None:
209 """
210 Find the common dtype for `blocks`.
211
212 Parameters
213 ----------
214 blocks : List[DtypeObj]
215
216 Returns
217 -------
218 dtype : np.dtype, ExtensionDtype, or None
219 None is returned when `blocks` is empty.
220 """
221 if not len(dtypes):
222 return None
223
224 return find_common_type(dtypes)