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