1"""
2Boilerplate functions used in defining binary operations.
3"""
4
5from __future__ import annotations
6
7from functools import wraps
8from typing import TYPE_CHECKING
9
10from pandas._libs.lib import item_from_zerodim
11from pandas._libs.missing import is_matching_na
12
13from pandas.core.dtypes.generic import (
14 ABCExtensionArray,
15 ABCIndex,
16 ABCSeries,
17)
18
19from pandas.core.construction import (
20 ensure_wrapped_if_datetimelike,
21 sanitize_array,
22)
23
24if TYPE_CHECKING:
25 from collections.abc import Callable
26
27 from pandas._typing import F
28
29
30def unpack_zerodim_and_defer(name: str) -> Callable[[F], F]:
31 """
32 Boilerplate for pandas conventions in arithmetic and comparison methods.
33
34 Parameters
35 ----------
36 name : str
37
38 Returns
39 -------
40 decorator
41 """
42
43 def wrapper(method: F) -> F:
44 return _unpack_zerodim_and_defer(method, name)
45
46 return wrapper
47
48
49def _unpack_zerodim_and_defer(method: F, name: str) -> F:
50 """
51 Boilerplate for pandas conventions in arithmetic and comparison methods.
52
53 Ensure method returns NotImplemented when operating against "senior"
54 classes. Ensure zero-dimensional ndarrays are always unpacked.
55
56 Parameters
57 ----------
58 method : binary method
59 name : str
60
61 Returns
62 -------
63 method
64 """
65 is_logical = name.strip("_") in ["or", "xor", "and", "ror", "rxor", "rand"]
66
67 @wraps(method)
68 def new_method(self, other):
69 prio = getattr(other, "__pandas_priority__", None)
70 if prio is not None:
71 if prio > self.__pandas_priority__:
72 # e.g. other is DataFrame while self is Index/Series/EA
73 return NotImplemented
74
75 other = item_from_zerodim(other)
76 if (
77 isinstance(self, ABCExtensionArray)
78 and isinstance(other, list)
79 and not is_logical
80 ):
81 # See GH#62423
82 other = sanitize_array(other, None)
83 other = ensure_wrapped_if_datetimelike(other)
84
85 return method(self, other)
86
87 # error: Incompatible return value type (got "Callable[[Any, Any], Any]",
88 # expected "F")
89 return new_method # type: ignore[return-value]
90
91
92def get_op_result_name(left, right):
93 """
94 Find the appropriate name to pin to an operation result. This result
95 should always be either an Index or a Series.
96
97 Parameters
98 ----------
99 left : {Series, Index}
100 right : object
101
102 Returns
103 -------
104 name : object
105 Usually a string
106 """
107 if isinstance(right, (ABCSeries, ABCIndex)):
108 name = _maybe_match_name(left, right)
109 else:
110 name = left.name
111 return name
112
113
114def _maybe_match_name(a, b):
115 """
116 Try to find a name to attach to the result of an operation between
117 a and b. If only one of these has a `name` attribute, return that
118 name. Otherwise return a consensus name if they match or None if
119 they have different names.
120
121 Parameters
122 ----------
123 a : object
124 b : object
125
126 Returns
127 -------
128 name : str or None
129
130 See Also
131 --------
132 pandas.core.common.consensus_name_attr
133 """
134 a_has = hasattr(a, "name")
135 b_has = hasattr(b, "name")
136 if a_has and b_has:
137 try:
138 if a.name == b.name:
139 return a.name
140 elif is_matching_na(a.name, b.name):
141 # e.g. both are np.nan
142 return a.name
143 else:
144 return None
145 except TypeError:
146 # pd.NA
147 if is_matching_na(a.name, b.name):
148 return a.name
149 return None
150 except ValueError:
151 # e.g. np.int64(1) vs (np.int64(1), np.int64(2))
152 return None
153 elif a_has:
154 return a.name
155 elif b_has:
156 return b.name
157 return None