# -*- coding: utf-8 -*- """ accessor.py contains base classes for implementing accessor properties that can be mixed into or pinned onto other pandas classes. """ import warnings from pandas.util._decorators import Appender class DirNamesMixin(object): _accessors = frozenset() _deprecations = frozenset( ['asobject', 'base', 'data', 'flags', 'itemsize', 'strides']) def _dir_deletions(self): """ delete unwanted __dir__ for this object """ return self._accessors | self._deprecations def _dir_additions(self): """ add additional __dir__ for this object """ rv = set() for accessor in self._accessors: try: getattr(self, accessor) rv.add(accessor) except AttributeError: pass return rv def __dir__(self): """ Provide method name lookup and completion Only provide 'public' methods """ rv = set(dir(type(self))) rv = (rv - self._dir_deletions()) | self._dir_additions() return sorted(rv) class PandasDelegate(object): """ an abstract base class for delegating methods/properties """ 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) def delegate_names(delegate, accessors, typ, overwrite=False): """ Add delegated names to a class using a class decorator. This provides an alternative usage to directly calling `_add_delegate_accessors` below a class definition. Parameters ---------- delegate : object the class to get methods/properties & doc-strings acccessors : Sequence[str] List of accessor to add typ : {'property', 'method'} overwrite : boolean, default False overwrite the method/property in the target class if it exists Returns ------- callable A class decorator. Examples -------- @delegate_names(Categorical, ["categories", "ordered"], "property") class CategoricalAccessor(PandasDelegate): [...] """ def add_delegate_accessors(cls): cls._add_delegate_accessors(delegate, accessors, typ, overwrite=overwrite) return cls return add_delegate_accessors # Ported with modifications from xarray # https://github.com/pydata/xarray/blob/master/xarray/core/extensions.py # 1. We don't need to catch and re-raise AttributeErrors as RuntimeErrors # 2. We use a UserWarning instead of a custom Warning class CachedAccessor(object): """ Custom property-like object (descriptor) for caching accessors. Parameters ---------- name : str The namespace this will be accessed under, e.g. ``df.foo`` accessor : cls The class with the extension methods. The class' __init__ method should expect one of a ``Series``, ``DataFrame`` or ``Index`` as the single argument ``data`` """ def __init__(self, name, accessor): self._name = name self._accessor = accessor def __get__(self, obj, cls): if obj is None: # we're accessing the attribute of the class, i.e., Dataset.geo return self._accessor accessor_obj = self._accessor(obj) # Replace the property with the accessor object. Inspired by: # http://www.pydanny.com/cached-property.html # We need to use object.__setattr__ because we overwrite __setattr__ on # NDFrame object.__setattr__(obj, self._name, accessor_obj) return accessor_obj def _register_accessor(name, cls): def decorator(accessor): if hasattr(cls, name): warnings.warn( 'registration of accessor {!r} under name {!r} for type ' '{!r} is overriding a preexisting attribute with the same ' 'name.'.format(accessor, name, cls), UserWarning, stacklevel=2) setattr(cls, name, CachedAccessor(name, accessor)) cls._accessors.add(name) return accessor return decorator _doc = """\ Register a custom accessor on %(klass)s objects. Parameters ---------- name : str Name under which the accessor should be registered. A warning is issued if this name conflicts with a preexisting attribute. See Also -------- %(others)s Notes ----- When accessed, your accessor will be initialized with the pandas object the user is interacting with. So the signature must be .. code-block:: python def __init__(self, pandas_object): # noqa: E999 ... For consistency with pandas methods, you should raise an ``AttributeError`` if the data passed to your accessor has an incorrect dtype. >>> pd.Series(['a', 'b']).dt Traceback (most recent call last): ... AttributeError: Can only use .dt accessor with datetimelike values Examples -------- In your library code:: import pandas as pd @pd.api.extensions.register_dataframe_accessor("geo") class GeoAccessor(object): def __init__(self, pandas_obj): self._obj = pandas_obj @property def center(self): # return the geographic center point of this DataFrame lat = self._obj.latitude lon = self._obj.longitude return (float(lon.mean()), float(lat.mean())) def plot(self): # plot this array's data on a map, e.g., using Cartopy pass Back in an interactive IPython session: >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), ... 'latitude': np.linspace(0, 20)}) >>> ds.geo.center (5.0, 10.0) >>> ds.geo.plot() # plots data on a map """ @Appender(_doc % dict(klass="DataFrame", others=("register_series_accessor, " "register_index_accessor"))) def register_dataframe_accessor(name): from pandas import DataFrame return _register_accessor(name, DataFrame) @Appender(_doc % dict(klass="Series", others=("register_dataframe_accessor, " "register_index_accessor"))) def register_series_accessor(name): from pandas import Series return _register_accessor(name, Series) @Appender(_doc % dict(klass="Index", others=("register_dataframe_accessor, " "register_series_accessor"))) def register_index_accessor(name): from pandas import Index return _register_accessor(name, Index)