Skip to content

WIP: ensure names are pinned correctly #24429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions pandas/core/arrays/sparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1706,12 +1706,11 @@ def sparse_arithmetic_method(self, other):

@classmethod
def _create_comparison_method(cls, op):
def cmp_method(self, other):
op_name = op.__name__

if op_name in {'and_', 'or_'}:
op_name = op_name[:-1]
op_name = op.__name__
if op_name in {'and_', 'or_'}:
op_name = op_name[:-1]

def cmp_method(self, other):
if isinstance(other, (ABCSeries, ABCIndexClass)):
# Rely on pandas to unbox and dispatch to us.
return NotImplemented
Expand Down Expand Up @@ -1740,7 +1739,7 @@ def cmp_method(self, other):
fill_value=fill_value,
dtype=np.bool_)

name = '__{name}__'.format(name=op.__name__)
name = '__{name}__'.format(name=op_name)
return compat.set_function_name(cmp_method, name, cls)

@classmethod
Expand Down
42 changes: 27 additions & 15 deletions pandas/core/indexes/frozen.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@
from pandas.io.formats.printing import pprint_thing


class FrozenList(PandasObject, list):
def _make_disabled(name):
def _disabled(self, *args, **kwargs):
"""This method will not function because object is immutable."""
raise TypeError("'{cls}' does not support mutable operations."
.format(cls=self.__class__.__name__))

_disabled.__name__ = name
return _disabled


class FrozenList(PandasObject, list):
"""
Container that doesn't allow setting item *but*
because it's technically non-hashable, will be used
Expand Down Expand Up @@ -103,11 +112,6 @@ def __reduce__(self):
def __hash__(self):
return hash(tuple(self))

def _disabled(self, *args, **kwargs):
"""This method will not function because object is immutable."""
raise TypeError("'%s' does not support mutable operations." %
self.__class__.__name__)

def __unicode__(self):
return pprint_thing(self, quote_strings=True,
escape_chars=('\t', '\r', '\n'))
Expand All @@ -116,8 +120,16 @@ def __repr__(self):
return "%s(%s)" % (self.__class__.__name__,
str(self))

__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
pop = append = extend = remove = sort = insert = _disabled
__setitem__ = _make_disabled("__setitem__")
__setslice__ = _make_disabled("__setslice__")
__delitem__ = _make_disabled("__delitem__")
__delslice__ = _make_disabled("__delslice__")
pop = _make_disabled("pop")
append = _make_disabled("append")
extend = _make_disabled("extend")
remove = _make_disabled("remove")
sort = _make_disabled("sort")
insert = _make_disabled("insert")


class FrozenNDArray(PandasObject, np.ndarray):
Expand All @@ -133,13 +145,13 @@ def __new__(cls, data, dtype=None, copy=False):
res = np.array(data, dtype=dtype, copy=copy).view(cls)
return res

def _disabled(self, *args, **kwargs):
"""This method will not function because object is immutable."""
raise TypeError("'%s' does not support mutable operations." %
self.__class__)

__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
put = itemset = fill = _disabled
__setitem__ = _make_disabled("__setitem__")
__setslice__ = _make_disabled("__setslice__")
__delitem__ = _make_disabled("__delitem__")
__delslice__ = _make_disabled("__delslice__")
put = _make_disabled("put")
itemset = _make_disabled("itemset")
fill = _make_disabled("fill")

def _shallow_copy(self):
return self.view()
Expand Down
85 changes: 85 additions & 0 deletions pandas/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import print_function

from datetime import datetime, timedelta
import inspect
import re
import sys

Expand Down Expand Up @@ -1345,3 +1346,87 @@ def test_to_numpy_dtype(as_series):
expected = np.array(['2000-01-01T05', '2001-01-01T05'],
dtype='M8[ns]')
tm.assert_numpy_array_equal(result, expected)


def check_pinned_names(cls):
"""
Check that the any dynamically-defined methods have the correct
names, i.e. not 'wrapper'.
"""
special_cases = {
"isnull": "isna",
"notnull": "notna",
"iteritems": "items",
"__bool__": "__nonzero__",
"__div__": "__truediv__",
"__rdiv__": "__rtruediv__",
"__rmul__": "__mul__",
"__req__": "__eq__",

"T": "transpose",
"_unpickle_compat": "__setstate__",

# Questionable
"get_level_values": "_get_level_values",
"_xs": "xs",

# _Window
"agg": "aggregate",

# Categorical
"take": "take_nd",
"to_list": "tolist",

# Timestamp
"daysinmonth": "days_in_month",
"astimezone": "tz_convert",
"weekofyear": "week",
}
# special cases that we are more restrictive about, possibly fixing
# them at some point.
class_special_cases = {
"SparseArray": {
"get_values": "to_dense",
},
"FrozenList": {
"__add__": "union",
"__iadd__": "union",
"__imul__": "__mul__",
},
}
ignore = {
"_create_comparison_method",
}
if 'Subclassed' in cls.__name__:
# dummy classes defined in tests
return
for name in dir(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This iteration looks heavy. What time does it take to run through all cases defined in the test_pinned_names parametrization? Should test_pinned_names below get a pytest slow marker?

Would it not be better to do specific tests for the cases where methods are generated dynamically instead of this broader test?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What time does it take to run through all cases defined in the test_pinned_names parametrization? Should test_pinned_names below get a pytest slow marker?

Locally python -m pytest pandas/tests/test_base.py -k test_pinned_names takes 1.02 seconds.

Would it not be better to do specific tests for the cases where methods are generated dynamically instead of this broader test?

No, that's an invitation to have things fall through the cracks down the road.

try:
# e.g. properties may not be accessible on the class
attr = getattr(cls, name)
except Exception:
continue
if name in ignore:
continue
if inspect.ismethod(attr) or inspect.isfunction(attr):
# isfunction check is needed in py3
expected = special_cases.get(name, name)
result = attr.__name__

cls_kludge = class_special_cases.get(cls.__name__, {})
expected = cls_kludge.get(name, expected)

assert result in [name, expected], (result, expected, name,
cls.__name__)


@pytest.mark.parametrize('klass',
sorted(PandasObject.__subclasses__(),
key=lambda x: x.__name__) + [
pd.Timestamp,
pd.Period,
pd.Timedelta,
pd.Interval
])
def test_pinned_names(klass):
check_pinned_names(klass)