From b0a2a12f2137ca5f323119227ddbe51224841213 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 3 Aug 2017 09:54:02 -0700 Subject: [PATCH 1/2] Implement _make_accessor classmethod for PandasDelegate --- pandas/core/base.py | 11 ++++++++++- pandas/core/categorical.py | 7 +++++++ pandas/core/indexes/accessors.py | 8 ++++++++ pandas/core/series.py | 23 +++-------------------- pandas/core/strings.py | 30 +++++++++++++++--------------- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/pandas/core/base.py b/pandas/core/base.py index eb785b18bd02b..ec926dca28775 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -165,6 +165,12 @@ def __setattr__(self, key, value): class PandasDelegate(PandasObject): """ an abstract base class for delegating methods/properties """ + @classmethod + def _make_accessor(cls, data): + raise NotImplementedError("_make_accessor should be implemented" + "by subclass. It should return an instance" + "of `cls`.") + def _delegate_property_get(self, name, *args, **kwargs): raise TypeError("You cannot access the " "property {name}".format(name=name)) @@ -231,8 +237,11 @@ class AccessorProperty(object): """Descriptor for implementing accessor properties like Series.str """ - def __init__(self, accessor_cls, construct_accessor): + def __init__(self, accessor_cls, construct_accessor=None): self.accessor_cls = accessor_cls + if construct_accessor is None: + # We are assuming that `_make_accessor` classmethod exists. + construct_accessor = accessor_cls._make_accessor self.construct_accessor = construct_accessor self.__doc__ = accessor_cls.__doc__ diff --git a/pandas/core/categorical.py b/pandas/core/categorical.py index 1392ad2f011db..230361931125e 100644 --- a/pandas/core/categorical.py +++ b/pandas/core/categorical.py @@ -2061,6 +2061,13 @@ def _delegate_method(self, name, *args, **kwargs): if res is not None: return Series(res, index=self.index) + @classmethod + def _make_accessor(cls, data): + if not is_categorical_dtype(data.dtype): + raise AttributeError("Can only use .cat accessor with a " + "'category' dtype") + return CategoricalAccessor(data.values, data.index) + CategoricalAccessor._add_delegate_accessors(delegate=Categorical, accessors=["categories", diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py index f1fb9a8ad93a7..92c4287fe97b4 100644 --- a/pandas/core/indexes/accessors.py +++ b/pandas/core/indexes/accessors.py @@ -243,3 +243,11 @@ class CombinedDatetimelikeProperties(DatetimeProperties, TimedeltaProperties): # the Series.dt class property. For Series objects, .dt will always be one # of the more specific classes above. __doc__ = DatetimeProperties.__doc__ + + @classmethod + def _make_accessor(cls, data): + try: + return maybe_to_datetimelike(data) + except Exception: + raise AttributeError("Can only use .dt accessor with datetimelike " + "values") diff --git a/pandas/core/series.py b/pandas/core/series.py index 60d268c89a9d7..5f76fe1bdf7c7 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -55,8 +55,7 @@ from pandas.core.internals import SingleBlockManager from pandas.core.categorical import Categorical, CategoricalAccessor import pandas.core.strings as strings -from pandas.core.indexes.accessors import ( - maybe_to_datetimelike, CombinedDatetimelikeProperties) +from pandas.core.indexes.accessors import CombinedDatetimelikeProperties from pandas.core.indexes.datetimes import DatetimeIndex from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.core.indexes.period import PeriodIndex @@ -2912,27 +2911,11 @@ def to_period(self, freq=None, copy=True): # ------------------------------------------------------------------------- # Datetimelike delegation methods - - def _make_dt_accessor(self): - try: - return maybe_to_datetimelike(self) - except Exception: - raise AttributeError("Can only use .dt accessor with datetimelike " - "values") - - dt = base.AccessorProperty(CombinedDatetimelikeProperties, - _make_dt_accessor) + dt = base.AccessorProperty(CombinedDatetimelikeProperties) # ------------------------------------------------------------------------- # Categorical methods - - def _make_cat_accessor(self): - if not is_categorical_dtype(self.dtype): - raise AttributeError("Can only use .cat accessor with a " - "'category' dtype") - return CategoricalAccessor(self.values, self.index) - - cat = base.AccessorProperty(CategoricalAccessor, _make_cat_accessor) + cat = base.AccessorProperty(CategoricalAccessor) def _dir_deletions(self): return self._accessors diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 30465561a911c..0b1db0277eee3 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -1890,18 +1890,14 @@ def rindex(self, sub, start=0, end=None): docstring=_shared_docs['ismethods'] % _shared_docs['isdecimal']) - -class StringAccessorMixin(object): - """ Mixin to add a `.str` acessor to the class.""" - - # string methods - def _make_str_accessor(self): + @classmethod + def _make_accessor(cls, data): from pandas.core.index import Index - if (isinstance(self, ABCSeries) and - not ((is_categorical_dtype(self.dtype) and - is_object_dtype(self.values.categories)) or - (is_object_dtype(self.dtype)))): + if (isinstance(data, ABCSeries) and + not ((is_categorical_dtype(data.dtype) and + is_object_dtype(data.values.categories)) or + (is_object_dtype(data.dtype)))): # it's neither a string series not a categorical series with # strings inside the categories. # this really should exclude all series with any non-string values @@ -1910,23 +1906,27 @@ def _make_str_accessor(self): raise AttributeError("Can only use .str accessor with string " "values, which use np.object_ dtype in " "pandas") - elif isinstance(self, Index): + elif isinstance(data, Index): # can't use ABCIndex to exclude non-str # see scc/inferrence.pyx which can contain string values allowed_types = ('string', 'unicode', 'mixed', 'mixed-integer') - if self.inferred_type not in allowed_types: + if data.inferred_type not in allowed_types: message = ("Can only use .str accessor with string values " "(i.e. inferred_type is 'string', 'unicode' or " "'mixed')") raise AttributeError(message) - if self.nlevels > 1: + if data.nlevels > 1: message = ("Can only use .str accessor with Index, not " "MultiIndex") raise AttributeError(message) - return StringMethods(self) + return StringMethods(data) + + +class StringAccessorMixin(object): + """ Mixin to add a `.str` acessor to the class.""" - str = AccessorProperty(StringMethods, _make_str_accessor) + str = AccessorProperty(StringMethods) def _dir_additions(self): return set() From 9c0ae3f756cef3ecdb38c0144139843d1ee929d0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 3 Aug 2017 17:05:33 -0700 Subject: [PATCH 2/2] Fixups suggested by reviewers --- pandas/core/base.py | 10 ++++------ pandas/core/indexes/accessors.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pandas/core/base.py b/pandas/core/base.py index ec926dca28775..8f21e3125a27e 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -167,8 +167,8 @@ class PandasDelegate(PandasObject): @classmethod def _make_accessor(cls, data): - raise NotImplementedError("_make_accessor should be implemented" - "by subclass. It should return an instance" + raise AbstractMethodError("_make_accessor should be implemented" + "by subclass and return an instance" "of `cls`.") def _delegate_property_get(self, name, *args, **kwargs): @@ -239,10 +239,8 @@ class AccessorProperty(object): def __init__(self, accessor_cls, construct_accessor=None): self.accessor_cls = accessor_cls - if construct_accessor is None: - # We are assuming that `_make_accessor` classmethod exists. - construct_accessor = accessor_cls._make_accessor - self.construct_accessor = construct_accessor + self.construct_accessor = (construct_accessor or + accessor_cls._make_accessor) self.__doc__ = accessor_cls.__doc__ def __get__(self, instance, owner=None): diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py index 92c4287fe97b4..ce3143b342cec 100644 --- a/pandas/core/indexes/accessors.py +++ b/pandas/core/indexes/accessors.py @@ -249,5 +249,5 @@ def _make_accessor(cls, data): try: return maybe_to_datetimelike(data) except Exception: - raise AttributeError("Can only use .dt accessor with datetimelike " - "values") + raise AttributeError("Can only use .dt accessor with " + "datetimelike values")