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