Skip to content
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

WIP: Refactor accessors, unify usage, make "recipe" #17042

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b77e103
Move PandasDelegate and AccessorProperty; update imports
jbrockmendel Jul 19, 2017
dbc149d
Move apply _shared_docs to functions and attach to methods with copy
jbrockmendel Jul 20, 2017
3c77d94
Implement _make_accessor as classmethod on StringMethods
jbrockmendel Jul 20, 2017
19f7ff6
Add example/recipe
jbrockmendel Jul 20, 2017
d152421
Test to go along with example/recipe
jbrockmendel Jul 20, 2017
101e7e5
Transition to _make_accessor
jbrockmendel Jul 20, 2017
774a35d
Merge branch 'master' into accessory
jbrockmendel Jul 20, 2017
ccec595
Merge branch 'master' into accessory
jbrockmendel Jul 20, 2017
74e4539
Remove unused import that was causing a lint error
jbrockmendel Jul 20, 2017
953598a
merge pulled
jbrockmendel Jul 20, 2017
22d4892
Wrap long line
jbrockmendel Jul 22, 2017
014fae0
Refactor tests and documentation
jbrockmendel Jul 22, 2017
dd8315c
Typos, flake8 fixes, rearrange comments
jbrockmendel Jul 22, 2017
74a237b
Simplify categorical make_accessor args
jbrockmendel Jul 23, 2017
c931d4b
Rename PandasDelegate subclasses FooDelegate
jbrockmendel Jul 25, 2017
6c771b4
Revert import rearrangement; update names FooDelegate
jbrockmendel Jul 25, 2017
d3a4460
Deprecate StringAccessorMixin
jbrockmendel Jul 25, 2017
48f3b4d
Merge branch 'master' into accessory
jbrockmendel Jul 25, 2017
73a0633
lint fixes
jbrockmendel Jul 25, 2017
aa793ad
Merge branch 'accessory' of https://github.com/jbrockmendel/pandas in…
jbrockmendel Jul 25, 2017
264a7e7
Merge branch 'master' into accessory
jbrockmendel Sep 20, 2017
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
227 changes: 227 additions & 0 deletions pandas/core/accessors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pandas.core.base import PandasObject
from pandas.core.common import AbstractMethodError


class PandasDelegate(PandasObject):
""" an abstract base class for delegating methods/properties

Usage: To make a custom accessor, subclass `PandasDelegate`, overriding
the methods below. Then decorate this subclass with
`accessors.wrap_delegate_names` describing the methods and properties
that should be delegated.

Examples can be found in:

pandas.core.accessors.CategoricalAccessor
pandas.core.indexes.accessors (complicated example)
pandas.core.indexes.category.CategoricalIndex
pandas.core.strings.StringMethods
pandas.tests.test_accessors

"""

def __init__(self, values):
"""
The subclassed constructor will generally only be called by
_make_accessor. See _make_accessor.__doc__.
"""
self.values = values

@classmethod
def _make_accessor(cls, data): # pragma: no cover
"""
_make_accessor should implement any necessary validation on the
data argument to ensure that the properties/methods being
accessed will be available.

_make_accessor should return cls(data). If necessary, the arguments
to the constructor can be expanded. In this case, __init__ will
need to be overrided as well.

Parameters
----------
data : the underlying object being accessed, usually Series or Index

Returns
-------
Delegate : instance of PandasDelegate or subclass

"""
raise AbstractMethodError(
'It is up to subclasses to implement '
'_make_accessor. This does input validation on the object to '
'which the accessor is being pinned. '
'It should return an instance of `cls`.')
# return cls(data)

def _delegate_property_get(self, name, *args, **kwargs):
raise TypeError("You cannot access the "
"property {name}".format(name=name))

def _delegate_property_set(self, name, value, *args, **kwargs):
"""
Overriding _delegate_property_set is discouraged. It is generally
better to directly interact with the underlying data than to
alter it via the accessor.

An example that ignores this advice can be found in
tests.test_accessors.TestVectorizedAccessor
"""
raise TypeError("The property {name} cannot be set".format(name=name))

def _delegate_method(self, name, *args, **kwargs):
raise TypeError("You cannot call method {name}".format(name=name))


class AccessorProperty(object):
"""Descriptor for implementing accessor properties like Series.str
"""

def __init__(self, accessor_cls, construct_accessor=None):
self.accessor_cls = accessor_cls

if construct_accessor is None:
# accessor_cls._make_accessor must be a classmethod
construct_accessor = accessor_cls._make_accessor

self.construct_accessor = construct_accessor
self.__doc__ = accessor_cls.__doc__

def __get__(self, instance, owner=None):
if instance is None:
# this ensures that Series.str.<method> is well defined
return self.accessor_cls
return self.construct_accessor(instance)

def __set__(self, instance, value):
raise AttributeError("can't set attribute")

def __delete__(self, instance):
raise AttributeError("can't delete attribute")


class Delegator(object):
""" Delegator class contains methods that are used by PandasDelegate
and Accesor subclasses, but that so not ultimately belong in
the namespaces of user-facing classes.

Many of these methods *could* be module-level functions, but are
retained as staticmethods for organization purposes.
"""

@staticmethod
def create_delegator_property(name, delegate):
# Note: we really only need the `delegate` here for the docstring

def _getter(self):
return self._delegate_property_get(name)

def _setter(self, new_values):
return self._delegate_property_set(name, new_values)
# TODO: not hit in tests; not sure this is something we
# really want anyway

_getter.__name__ = name
_setter.__name__ = name
_doc = getattr(delegate, name).__doc__
return property(fget=_getter, fset=_setter, doc=_doc)

@staticmethod
def create_delegator_method(name, delegate):
# Note: we really only need the `delegate` here for the docstring

def func(self, *args, **kwargs):
return self._delegate_method(name, *args, **kwargs)

func.__name__ = name
func.__doc__ = getattr(delegate, name).__doc__
return func

@staticmethod
def delegate_names(delegate, accessors, typ, overwrite=False):
"""
delegate_names decorates class definitions, e.g:

@delegate_names(Categorical, ["categories", "ordered"], "property")
class CategoricalAccessor(PandasDelegate):

@classmethod
def _make_accessor(cls, data):
[...]


The motivation is that we would like to keep as much of a class's
internals inside the class definition. For things that we cannot
keep directly in the class definition, a decorator is more directly
tied to the definition than a method call outside the definition.

"""
# Note: we really only need the `delegate` here for the docstring

def add_delegate_accessors(cls):
"""
add accessors to cls from the delegate class

Parameters
----------
cls : the class to add the methods/properties to
delegate : the class to get methods/properties & doc-strings
acccessors : string list of accessors to add
typ : 'property' or 'method'
overwrite : boolean, default False
overwrite the method/property in the target class if it exists
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe some asserts to valid types of the accessors and such

for name in accessors:
if typ == "property":
func = Delegator.create_delegator_property(name, delegate)
else:
func = Delegator.create_delegator_method(name, delegate)

# don't overwrite existing methods/properties unless
# specifically told to do so
if overwrite or not hasattr(cls, name):
setattr(cls, name, func)

return cls

return add_delegate_accessors


wrap_delegate_names = Delegator.delegate_names
# TODO: the `delegate` arg to `wrap_delegate_names` is really only relevant
# for a docstring. It'd be nice if we didn't require it and could duck-type
# instead.

# TODO: There are 2-3 implementations of `_delegate_method`
# and `_delegate_property` that are common enough that we should consider
# making them the defaults. First, if the series being accessed has `name`
# method/property:
#
# def _delegate_method(self, name, *args, **kwargs):
# result = getattr(self.values, name)(*args, **kwargs)
# return result
#
# def _delegate_property_get(self, name):
# result = getattr(self.values, name)
# return result
#
#
# Alternately if the series being accessed does not have this attribute,
# but is a series of objects that do have the attribute:
#
# def _delegate_method(self, name, *args, **kwargs):
# meth = lambda x: getattr(x, name)(*args, **kwargs)
# return self.values.apply(meth)
#
# def _delegate_property_get(self, name):
# prop = lambda x: getattr(x, name)
# return self.values.apply(prop)
#
#
# `apply` would need to be changed to `map` if self.values is an Index.
#
# The third thing to consider moving into the general case is
# core.strings.StringMethods._wrap_result, which handles a bunch of cases
# for how to wrap delegated outputs.
94 changes: 0 additions & 94 deletions pandas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,100 +153,6 @@ def __setattr__(self, key, value):
object.__setattr__(self, key, value)


class PandasDelegate(PandasObject):
""" an abstract base class for delegating methods/properties """

@classmethod
def _make_accessor(cls, data):
raise AbstractMethodError("_make_accessor should be implemented"
"by subclass and return an instance"
"of `cls`.")

def _delegate_property_get(self, name, *args, **kwargs):
raise TypeError("You cannot access the "
"property {name}".format(name=name))

def _delegate_property_set(self, name, value, *args, **kwargs):
raise TypeError("The property {name} cannot be set".format(name=name))

def _delegate_method(self, name, *args, **kwargs):
raise TypeError("You cannot call method {name}".format(name=name))

@classmethod
def _add_delegate_accessors(cls, delegate, accessors, typ,
overwrite=False):
"""
add accessors to cls from the delegate class

Parameters
----------
cls : the class to add the methods/properties to
delegate : the class to get methods/properties & doc-strings
acccessors : string list of accessors to add
typ : 'property' or 'method'
overwrite : boolean, default False
overwrite the method/property in the target class if it exists
"""

def _create_delegator_property(name):

def _getter(self):
return self._delegate_property_get(name)

def _setter(self, new_values):
return self._delegate_property_set(name, new_values)

_getter.__name__ = name
_setter.__name__ = name

return property(fget=_getter, fset=_setter,
doc=getattr(delegate, name).__doc__)

def _create_delegator_method(name):

def f(self, *args, **kwargs):
return self._delegate_method(name, *args, **kwargs)

f.__name__ = name
f.__doc__ = getattr(delegate, name).__doc__

return f

for name in accessors:

if typ == 'property':
f = _create_delegator_property(name)
else:
f = _create_delegator_method(name)

# don't overwrite existing methods/properties
if overwrite or not hasattr(cls, name):
setattr(cls, name, f)


class AccessorProperty(object):
"""Descriptor for implementing accessor properties like Series.str
"""

def __init__(self, accessor_cls, construct_accessor=None):
self.accessor_cls = accessor_cls
self.construct_accessor = (construct_accessor or
accessor_cls._make_accessor)
self.__doc__ = accessor_cls.__doc__

def __get__(self, instance, owner=None):
if instance is None:
# this ensures that Series.str.<method> is well defined
return self.accessor_cls
return self.construct_accessor(instance)

def __set__(self, instance, value):
raise AttributeError("can't set attribute")

def __delete__(self, instance):
raise AttributeError("can't delete attribute")


class GroupByError(Exception):
pass

Expand Down
Loading