1"""
2Templates for invalid operations.
3"""
4
5from __future__ import annotations
6
7import datetime
8import operator
9from typing import (
10 TYPE_CHECKING,
11 Any,
12 NoReturn,
13)
14
15import numpy as np
16
17from pandas._libs import lib
18from pandas._libs.missing import NAType
19from pandas._libs.tslibs import (
20 BaseOffset,
21 Period,
22)
23
24from pandas.core.dtypes.dtypes import (
25 ArrowDtype,
26 CategoricalDtype,
27 PeriodDtype,
28)
29from pandas.core.dtypes.generic import (
30 ABCDataFrame,
31 ABCExtensionArray,
32 ABCIndex,
33 ABCNumpyExtensionArray,
34 ABCSeries,
35)
36
37if TYPE_CHECKING:
38 from collections.abc import Callable
39
40 from pandas._typing import (
41 ArrayLike,
42 Scalar,
43 npt,
44 )
45
46
47_LOGICAL_UFUNCS = frozenset(
48 {np.logical_and, np.logical_or, np.logical_xor, np.logical_not}
49)
50
51# Timestamp and NaT subclass datetime.date, Timedelta subclasses datetime.timedelta
52_DATETIMELIKE_SCALARS = (
53 datetime.date,
54 datetime.time,
55 datetime.timedelta,
56 np.datetime64,
57 np.timedelta64,
58 Period,
59 BaseOffset,
60)
61
62# a ufunc against one of these stays inside pandas: every _HANDLED_TYPES contains
63# np.ndarray, NA matches no ABC (see test_logical_ufunc_na_scalar_datetimelike),
64# and a pandas operand that one array does defer to re-enters this guard
65_NON_DEFERRING = (
66 np.ndarray,
67 ABCDataFrame,
68 ABCExtensionArray,
69 # NumpyExtensionArray's _typ is not one of ABCExtensionArray's
70 ABCNumpyExtensionArray,
71 ABCIndex,
72 ABCSeries,
73 NAType,
74)
75
76
77def _defers_to(obj: object) -> bool:
78 """
79 Whether a ufunc operand is one pandas hands the operation off to.
80 """
81 array_ufunc = getattr(type(obj), "__array_ufunc__", None)
82 return (
83 array_ufunc is not None
84 and array_ufunc is not np.ndarray.__array_ufunc__
85 # issubclass, not isinstance: the ABCs read `_typ` off whatever they are
86 # handed, and a symbolic operand answers that with an expression that
87 # then has no truth value, see test_logical_ufunc_symbolic_operand
88 and not issubclass(type(obj), _NON_DEFERRING)
89 )
90
91
92def invalid_comparison(
93 left: ArrayLike,
94 right: ArrayLike | list[Any] | range | Scalar | NAType,
95 op: Callable[[Any, Any], bool],
96) -> npt.NDArray[np.bool_]:
97 """
98 If a comparison has mismatched types and is not necessarily meaningful,
99 follow python3 conventions by:
100
101 - returning all-False for equality
102 - returning all-True for inequality
103 - raising TypeError otherwise
104
105 Parameters
106 ----------
107 left : array-like
108 right : scalar, array-like
109 op : operator.{eq, ne, lt, le, gt}
110
111 Raises
112 ------
113 TypeError : on inequality comparisons
114 """
115 if op is operator.eq:
116 res_values = np.zeros(left.shape, dtype=bool)
117 elif op is operator.ne:
118 res_values = np.ones(left.shape, dtype=bool)
119 else:
120 typ = type(right).__name__
121 raise TypeError(f"Invalid comparison between dtype={left.dtype} and {typ}")
122 return res_values
123
124
125def _is_datetimelike_array(obj: object) -> bool:
126 if lib.is_scalar(obj):
127 return False
128 dtype = getattr(obj, "dtype", None)
129 if dtype is None:
130 return False
131 return _is_datetimelike_dtype(dtype)
132
133
134def _is_datetimelike_dtype(dtype: object) -> bool:
135 kind = getattr(dtype, "kind", None)
136 if not isinstance(kind, str) or kind in "biufc":
137 # no numeric or bool dtype is datetimelike, and logical_op is hot. A
138 # third-party dtype may have no kind at all, or one that is not a string,
139 # and must fall through rather than crash, see test_logical_op_third_party
140 return False
141 if isinstance(dtype, CategoricalDtype) and dtype.categories is not None:
142 # a Categorical hides its categories behind kind "O"
143 return _is_datetimelike_dtype(dtype.categories.dtype)
144 if isinstance(dtype, ArrowDtype):
145 import pyarrow as pa
146
147 pa_type = dtype.pyarrow_dtype
148 if pa.types.is_dictionary(pa_type) or pa.types.is_run_end_encoded(pa_type):
149 # the arrow spellings of the CategoricalDtype unwrap above
150 pa_type = pa_type.value_type
151 # kind is "O" for those two, for time32/time64 and for month_day_nano_interval
152 return pa.types.is_temporal(pa_type)
153 # kind covers numpy M8/m8 and DatetimeTZDtype; PeriodDtype has kind "O" and needs
154 # naming. Plain object dtype is excluded on purpose, see
155 # test_logical_op_object_dtype_still_truthy
156 return kind in "mM" or isinstance(dtype, PeriodDtype)
157
158
159def _operand_repr(obj: object) -> str:
160 dtype = getattr(obj, "dtype", None)
161 if dtype is None:
162 return f"object of type {type(obj).__name__}"
163 return f"dtype '{dtype}'"
164
165
166def disallow_datetimelike_logical_op(
167 left: ArrayLike, right: Any, op: Callable[[Any, Any], Any]
168) -> None:
169 """
170 Raise TypeError if either operand of a logical op is a datetimelike array.
171
172 Parameters
173 ----------
174 left : array-like
175 right : array-like or scalar
176 op : operator.{and_, or_, xor}
177 Or one of the reversed variants from roperator.
178
179 Raises
180 ------
181 TypeError : if either operand is a datetimelike array, including a categorical
182 of one
183 """
184 # GH#68452 these have no truth value; casting them to bool would make every
185 # entry, NaT included, True
186 if _is_datetimelike_array(left) or _is_datetimelike_array(right):
187 raise TypeError(
188 f"operation '{op.__name__}' not supported for "
189 f"{_operand_repr(left)} with {_operand_repr(right)}"
190 )
191
192
193def disallow_datetimelike_logical_ufunc(
194 ufunc: np.ufunc, inputs: tuple[Any, ...]
195) -> None:
196 """
197 Raise TypeError if a logical ufunc is applied to datetimelike data.
198
199 These are not in ``UFUNC_ALIASES`` -- they differ from ``&``/``|``/``^`` on
200 integer data -- so they never reach :func:`disallow_datetimelike_logical_op`
201 and need their own guard. GH#68524
202
203 Parameters
204 ----------
205 ufunc : numpy.ufunc
206 inputs : tuple
207 The ufunc's operands, which may include DataFrames.
208
209 Raises
210 ------
211 TypeError : if any operand is datetimelike
212 """
213 if ufunc not in _LOGICAL_UFUNCS:
214 return
215
216 if any(_defers_to(obj) for obj in inputs):
217 # pandas defers the whole op, so raising for any operand takes it away,
218 # see test_logical_ufunc_third_party_datetimelike
219 return
220
221 for obj in inputs:
222 if isinstance(obj, ABCDataFrame):
223 # a DataFrame has no dtype of its own, and with two inputs
224 # array_ufunc np.asarray()s it before any column-level guard runs.
225 # Scanned per block rather than per column: this runs on every
226 # logical ufunc call
227 dtype = next(
228 (
229 blk.dtype
230 for blk in obj._mgr.blocks
231 if _is_datetimelike_dtype(blk.dtype)
232 ),
233 None,
234 )
235 if dtype is None:
236 continue
237 descr = f"with dtype {dtype}"
238 elif _is_datetimelike_array(obj):
239 descr = f"with dtype {obj.dtype}"
240 elif isinstance(obj, _DATETIMELIKE_SCALARS):
241 # GH#68452 left scalar operands to the paths that already rejected
242 # them for &/|/^; no such path exists on the ufunc side
243 descr = f"of type {type(obj).__name__}"
244 else:
245 continue
246 raise TypeError(f"Object {descr} cannot perform the numpy op {ufunc.__name__}")
247
248
249def make_invalid_op(name: str) -> Callable[..., NoReturn]:
250 """
251 Return a binary method that always raises a TypeError.
252
253 Parameters
254 ----------
255 name : str
256
257 Returns
258 -------
259 invalid_op : function
260 """
261
262 def invalid_op(self: object, other: object = None) -> NoReturn:
263 typ = type(self).__name__
264 raise TypeError(f"cannot perform {name} with this index type: {typ}")
265
266 invalid_op.__name__ = name
267 return invalid_op