-
-
Notifications
You must be signed in to change notification settings - Fork 18.3k
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
Closed
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 dbc149d
Move apply _shared_docs to functions and attach to methods with copy
jbrockmendel 3c77d94
Implement _make_accessor as classmethod on StringMethods
jbrockmendel 19f7ff6
Add example/recipe
jbrockmendel d152421
Test to go along with example/recipe
jbrockmendel 101e7e5
Transition to _make_accessor
jbrockmendel 774a35d
Merge branch 'master' into accessory
jbrockmendel ccec595
Merge branch 'master' into accessory
jbrockmendel 74e4539
Remove unused import that was causing a lint error
jbrockmendel 953598a
merge pulled
jbrockmendel 22d4892
Wrap long line
jbrockmendel 014fae0
Refactor tests and documentation
jbrockmendel dd8315c
Typos, flake8 fixes, rearrange comments
jbrockmendel 74a237b
Simplify categorical make_accessor args
jbrockmendel c931d4b
Rename PandasDelegate subclasses FooDelegate
jbrockmendel 6c771b4
Revert import rearrangement; update names FooDelegate
jbrockmendel d3a4460
Deprecate StringAccessorMixin
jbrockmendel 48f3b4d
Merge branch 'master' into accessory
jbrockmendel 73a0633
lint fixes
jbrockmendel aa793ad
Merge branch 'accessory' of https://github.com/jbrockmendel/pandas in…
jbrockmendel 264a7e7
Merge branch 'master' into accessory
jbrockmendel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
""" | ||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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