152 lines
3.6 KiB
Python
152 lines
3.6 KiB
Python
"""
|
|
Boilerplate functions used in defining binary operations.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from functools import wraps
|
|
import sys
|
|
from typing import Callable
|
|
|
|
from pandas._libs.lib import item_from_zerodim
|
|
from pandas._libs.missing import is_matching_na
|
|
from pandas._typing import F
|
|
|
|
from pandas.core.dtypes.generic import (
|
|
ABCDataFrame,
|
|
ABCIndex,
|
|
ABCSeries,
|
|
)
|
|
|
|
|
|
def unpack_zerodim_and_defer(name: str) -> Callable[[F], F]:
|
|
"""
|
|
Boilerplate for pandas conventions in arithmetic and comparison methods.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
|
|
Returns
|
|
-------
|
|
decorator
|
|
"""
|
|
|
|
def wrapper(method: F) -> F:
|
|
return _unpack_zerodim_and_defer(method, name)
|
|
|
|
return wrapper
|
|
|
|
|
|
def _unpack_zerodim_and_defer(method, name: str):
|
|
"""
|
|
Boilerplate for pandas conventions in arithmetic and comparison methods.
|
|
|
|
Ensure method returns NotImplemented when operating against "senior"
|
|
classes. Ensure zero-dimensional ndarrays are always unpacked.
|
|
|
|
Parameters
|
|
----------
|
|
method : binary method
|
|
name : str
|
|
|
|
Returns
|
|
-------
|
|
method
|
|
"""
|
|
if sys.version_info < (3, 9):
|
|
from pandas.util._str_methods import (
|
|
removeprefix,
|
|
removesuffix,
|
|
)
|
|
|
|
stripped_name = removesuffix(removeprefix(name, "__"), "__")
|
|
else:
|
|
stripped_name = name.removeprefix("__").removesuffix("__")
|
|
is_cmp = stripped_name in {"eq", "ne", "lt", "le", "gt", "ge"}
|
|
|
|
@wraps(method)
|
|
def new_method(self, other):
|
|
if is_cmp and isinstance(self, ABCIndex) and isinstance(other, ABCSeries):
|
|
# For comparison ops, Index does *not* defer to Series
|
|
pass
|
|
else:
|
|
for cls in [ABCDataFrame, ABCSeries, ABCIndex]:
|
|
if isinstance(self, cls):
|
|
break
|
|
if isinstance(other, cls):
|
|
return NotImplemented
|
|
|
|
other = item_from_zerodim(other)
|
|
|
|
return method(self, other)
|
|
|
|
return new_method
|
|
|
|
|
|
def get_op_result_name(left, right):
|
|
"""
|
|
Find the appropriate name to pin to an operation result. This result
|
|
should always be either an Index or a Series.
|
|
|
|
Parameters
|
|
----------
|
|
left : {Series, Index}
|
|
right : object
|
|
|
|
Returns
|
|
-------
|
|
name : object
|
|
Usually a string
|
|
"""
|
|
if isinstance(right, (ABCSeries, ABCIndex)):
|
|
name = _maybe_match_name(left, right)
|
|
else:
|
|
name = left.name
|
|
return name
|
|
|
|
|
|
def _maybe_match_name(a, b):
|
|
"""
|
|
Try to find a name to attach to the result of an operation between
|
|
a and b. If only one of these has a `name` attribute, return that
|
|
name. Otherwise return a consensus name if they match or None if
|
|
they have different names.
|
|
|
|
Parameters
|
|
----------
|
|
a : object
|
|
b : object
|
|
|
|
Returns
|
|
-------
|
|
name : str or None
|
|
|
|
See Also
|
|
--------
|
|
pandas.core.common.consensus_name_attr
|
|
"""
|
|
a_has = hasattr(a, "name")
|
|
b_has = hasattr(b, "name")
|
|
if a_has and b_has:
|
|
try:
|
|
if a.name == b.name:
|
|
return a.name
|
|
elif is_matching_na(a.name, b.name):
|
|
# e.g. both are np.nan
|
|
return a.name
|
|
else:
|
|
return None
|
|
except TypeError:
|
|
# pd.NA
|
|
if is_matching_na(a.name, b.name):
|
|
return a.name
|
|
return None
|
|
except ValueError:
|
|
# e.g. np.int64(1) vs (np.int64(1), np.int64(2))
|
|
return None
|
|
elif a_has:
|
|
return a.name
|
|
elif b_has:
|
|
return b.name
|
|
return None
|