1"""
2Boilerplate functions used in defining binary operations.
3"""
4
5from __future__ import annotations
6
7from functools import wraps
8from typing import TYPE_CHECKING
9import warnings
10
11import numpy as np
12
13from pandas._libs.lib import (
14 is_list_like,
15 item_from_zerodim,
16)
17from pandas._libs.missing import is_matching_na
18from pandas.errors import Pandas4Warning
19from pandas.util._exceptions import find_stack_level
20
21from pandas.core.dtypes.generic import (
22 ABCDataFrame,
23 ABCExtensionArray,
24 ABCIndex,
25 ABCNumpyExtensionArray,
26 ABCSeries,
27)
28
29from pandas.core.construction import (
30 ensure_wrapped_if_datetimelike,
31 sanitize_array,
32)
33
34if TYPE_CHECKING:
35 from collections.abc import Callable
36
37 from pandas._typing import F
38
39
40def has_castable_attr(obj) -> bool:
41 attrs = ["__array__", "__dlpack__", "__arrow_c_array__", "__arrow_c_stream__"]
42 return any(hasattr(obj, name) for name in attrs)
43
44
45def raise_if_2d(other) -> None:
46 """
47 Reject a multi-dimensional operand before any dtype-specific conversion,
48 matching BaseMaskedArray (GH#62682).
49 """
50 # ABCExtensionArray does not match NumpyExtensionArray, whose _typ is
51 # "npy_extension" -- and that is the EA most likely to be 2-D (GH#62682)
52 if (
53 isinstance(other, (np.ndarray, ABCExtensionArray, ABCNumpyExtensionArray))
54 and other.ndim > 1
55 ):
56 raise NotImplementedError("can only perform ops with 1-d structures")
57
58
59def maybe_warn_listlike(other) -> None:
60 """
61 Warn when operating against a list-like that is neither a standard container
62 (``list``, ``np.ndarray``) nor a pandas object nor array-castable.
63
64 Such operations (e.g. with ``tuple``, ``range``, ``deque``) are deprecated
65 (GH#62423) and will treat ``other`` as scalar-like in a future version.
66 """
67 if (
68 is_list_like(other)
69 and not isinstance(
70 other,
71 (list, np.ndarray, ABCExtensionArray, ABCIndex, ABCSeries, ABCDataFrame),
72 )
73 and not has_castable_attr(other)
74 ):
75 warnings.warn(
76 f"Operation with {type(other).__name__} is deprecated. "
77 "In a future version these will be treated as scalar-like. "
78 "To retain the old behavior, explicitly wrap in a Series "
79 "instead.",
80 Pandas4Warning,
81 stacklevel=find_stack_level(),
82 )
83
84
85def unpack_zerodim_and_defer(name: str) -> Callable[[F], F]:
86 """
87 Boilerplate for pandas conventions in arithmetic and comparison methods.
88
89 Parameters
90 ----------
91 name : str
92
93 Returns
94 -------
95 decorator
96 """
97
98 def wrapper(method: F) -> F:
99 return _unpack_zerodim_and_defer(method, name)
100
101 return wrapper
102
103
104def _unpack_zerodim_and_defer(method: F, name: str) -> F:
105 """
106 Boilerplate for pandas conventions in arithmetic and comparison methods.
107
108 Ensure method returns NotImplemented when operating against "senior"
109 classes. Ensure zero-dimensional ndarrays are always unpacked.
110
111 Parameters
112 ----------
113 method : binary method
114 name : str
115
116 Returns
117 -------
118 method
119 """
120 is_logical = name.strip("_") in ["or", "xor", "and", "ror", "rxor", "rand"]
121
122 @wraps(method)
123 def new_method(self, other):
124 prio = getattr(other, "__pandas_priority__", None)
125 if prio is not None:
126 if prio > self.__pandas_priority__:
127 # e.g. other is DataFrame while self is Index/Series/EA
128 return NotImplemented
129
130 other = item_from_zerodim(other)
131 if (
132 isinstance(self, ABCExtensionArray)
133 and isinstance(other, list)
134 and not is_logical
135 ):
136 # See GH#62423
137 other = sanitize_array(other, None)
138 other = ensure_wrapped_if_datetimelike(other)
139
140 return method(self, other)
141
142 # error: Incompatible return value type (got "Callable[[Any, Any], Any]",
143 # expected "F")
144 return new_method # type: ignore[return-value]
145
146
147def get_op_result_name(left, right):
148 """
149 Find the appropriate name to pin to an operation result. This result
150 should always be either an Index or a Series.
151
152 Parameters
153 ----------
154 left : {Series, Index}
155 right : object
156
157 Returns
158 -------
159 name : object
160 Usually a string
161 """
162 if isinstance(right, (ABCSeries, ABCIndex)):
163 name = _maybe_match_name(left, right)
164 else:
165 name = left.name
166 return name
167
168
169def _maybe_match_name(a, b):
170 """
171 Try to find a name to attach to the result of an operation between
172 a and b. If only one of these has a `name` attribute, return that
173 name. Otherwise return a consensus name if they match or None if
174 they have different names.
175
176 Parameters
177 ----------
178 a : object
179 b : object
180
181 Returns
182 -------
183 name : str or None
184
185 See Also
186 --------
187 pandas.core.common.consensus_name_attr
188 """
189 a_has = hasattr(a, "name")
190 b_has = hasattr(b, "name")
191 if a_has and b_has:
192 try:
193 if a.name == b.name:
194 return a.name
195 elif is_matching_na(a.name, b.name):
196 # e.g. both are np.nan
197 return a.name
198 else:
199 return None
200 except TypeError:
201 # pd.NA
202 if is_matching_na(a.name, b.name):
203 return a.name
204 return None
205 except ValueError:
206 # e.g. np.int64(1) vs (np.int64(1), np.int64(2))
207 return None
208 elif a_has:
209 return a.name
210 elif b_has:
211 return b.name
212 return None