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