1"""
2
3accessor.py contains base classes for implementing accessor properties
4that can be mixed into or pinned onto other pandas classes.
5
6"""
7from __future__ import annotations
8
9from typing import (
10 Callable,
11 final,
12)
13import warnings
14
15from pandas.util._decorators import doc
16from pandas.util._exceptions import find_stack_level
17
18
19class DirNamesMixin:
20 _accessors: set[str] = set()
21 _hidden_attrs: frozenset[str] = frozenset()
22
23 @final
24 def _dir_deletions(self) -> set[str]:
25 """
26 Delete unwanted __dir__ for this object.
27 """
28 return self._accessors | self._hidden_attrs
29
30 def _dir_additions(self) -> set[str]:
31 """
32 Add additional __dir__ for this object.
33 """
34 return {accessor for accessor in self._accessors if hasattr(self, accessor)}
35
36 def __dir__(self) -> list[str]:
37 """
38 Provide method name lookup and completion.
39
40 Notes
41 -----
42 Only provide 'public' methods.
43 """
44 rv = set(super().__dir__())
45 rv = (rv - self._dir_deletions()) | self._dir_additions()
46 return sorted(rv)
47
48
49class PandasDelegate:
50 """
51 Abstract base class for delegating methods/properties.
52 """
53
54 def _delegate_property_get(self, name, *args, **kwargs):
55 raise TypeError(f"You cannot access the property {name}")
56
57 def _delegate_property_set(self, name, value, *args, **kwargs):
58 raise TypeError(f"The property {name} cannot be set")
59
60 def _delegate_method(self, name, *args, **kwargs):
61 raise TypeError(f"You cannot call method {name}")
62
63 @classmethod
64 def _add_delegate_accessors(
65 cls,
66 delegate,
67 accessors: list[str],
68 typ: str,
69 overwrite: bool = False,
70 accessor_mapping: Callable[[str], str] = lambda x: x,
71 raise_on_missing: bool = True,
72 ) -> None:
73 """
74 Add accessors to cls from the delegate class.
75
76 Parameters
77 ----------
78 cls
79 Class to add the methods/properties to.
80 delegate
81 Class to get methods/properties and doc-strings.
82 accessors : list of str
83 List of accessors to add.
84 typ : {'property', 'method'}
85 overwrite : bool, default False
86 Overwrite the method/property in the target class if it exists.
87 accessor_mapping: Callable, default lambda x: x
88 Callable to map the delegate's function to the cls' function.
89 raise_on_missing: bool, default True
90 Raise if an accessor does not exist on delegate.
91 False skips the missing accessor.
92 """
93
94 def _create_delegator_property(name):
95 def _getter(self):
96 return self._delegate_property_get(name)
97
98 def _setter(self, new_values):
99 return self._delegate_property_set(name, new_values)
100
101 _getter.__name__ = name
102 _setter.__name__ = name
103
104 return property(
105 fget=_getter,
106 fset=_setter,
107 doc=getattr(delegate, accessor_mapping(name)).__doc__,
108 )
109
110 def _create_delegator_method(name):
111 def f(self, *args, **kwargs):
112 return self._delegate_method(name, *args, **kwargs)
113
114 f.__name__ = name
115 f.__doc__ = getattr(delegate, accessor_mapping(name)).__doc__
116
117 return f
118
119 for name in accessors:
120 if (
121 not raise_on_missing
122 and getattr(delegate, accessor_mapping(name), None) is None
123 ):
124 continue
125
126 if typ == "property":
127 f = _create_delegator_property(name)
128 else:
129 f = _create_delegator_method(name)
130
131 # don't overwrite existing methods/properties
132 if overwrite or not hasattr(cls, name):
133 setattr(cls, name, f)
134
135
136def delegate_names(
137 delegate,
138 accessors: list[str],
139 typ: str,
140 overwrite: bool = False,
141 accessor_mapping: Callable[[str], str] = lambda x: x,
142 raise_on_missing: bool = True,
143):
144 """
145 Add delegated names to a class using a class decorator. This provides
146 an alternative usage to directly calling `_add_delegate_accessors`
147 below a class definition.
148
149 Parameters
150 ----------
151 delegate : object
152 The class to get methods/properties & doc-strings.
153 accessors : Sequence[str]
154 List of accessor to add.
155 typ : {'property', 'method'}
156 overwrite : bool, default False
157 Overwrite the method/property in the target class if it exists.
158 accessor_mapping: Callable, default lambda x: x
159 Callable to map the delegate's function to the cls' function.
160 raise_on_missing: bool, default True
161 Raise if an accessor does not exist on delegate.
162 False skips the missing accessor.
163
164 Returns
165 -------
166 callable
167 A class decorator.
168
169 Examples
170 --------
171 @delegate_names(Categorical, ["categories", "ordered"], "property")
172 class CategoricalAccessor(PandasDelegate):
173 [...]
174 """
175
176 def add_delegate_accessors(cls):
177 cls._add_delegate_accessors(
178 delegate,
179 accessors,
180 typ,
181 overwrite=overwrite,
182 accessor_mapping=accessor_mapping,
183 raise_on_missing=raise_on_missing,
184 )
185 return cls
186
187 return add_delegate_accessors
188
189
190# Ported with modifications from xarray
191# https://github.com/pydata/xarray/blob/master/xarray/core/extensions.py
192# 1. We don't need to catch and re-raise AttributeErrors as RuntimeErrors
193# 2. We use a UserWarning instead of a custom Warning
194
195
196class CachedAccessor:
197 """
198 Custom property-like object.
199
200 A descriptor for caching accessors.
201
202 Parameters
203 ----------
204 name : str
205 Namespace that will be accessed under, e.g. ``df.foo``.
206 accessor : cls
207 Class with the extension methods.
208
209 Notes
210 -----
211 For accessor, The class's __init__ method assumes that one of
212 ``Series``, ``DataFrame`` or ``Index`` as the
213 single argument ``data``.
214 """
215
216 def __init__(self, name: str, accessor) -> None:
217 self._name = name
218 self._accessor = accessor
219
220 def __get__(self, obj, cls):
221 if obj is None:
222 # we're accessing the attribute of the class, i.e., Dataset.geo
223 return self._accessor
224 accessor_obj = self._accessor(obj)
225 # Replace the property with the accessor object. Inspired by:
226 # https://www.pydanny.com/cached-property.html
227 # We need to use object.__setattr__ because we overwrite __setattr__ on
228 # NDFrame
229 object.__setattr__(obj, self._name, accessor_obj)
230 return accessor_obj
231
232
233@doc(klass="", others="")
234def _register_accessor(name, cls):
235 """
236 Register a custom accessor on {klass} objects.
237
238 Parameters
239 ----------
240 name : str
241 Name under which the accessor should be registered. A warning is issued
242 if this name conflicts with a preexisting attribute.
243
244 Returns
245 -------
246 callable
247 A class decorator.
248
249 See Also
250 --------
251 register_dataframe_accessor : Register a custom accessor on DataFrame objects.
252 register_series_accessor : Register a custom accessor on Series objects.
253 register_index_accessor : Register a custom accessor on Index objects.
254
255 Notes
256 -----
257 When accessed, your accessor will be initialized with the pandas object
258 the user is interacting with. So the signature must be
259
260 .. code-block:: python
261
262 def __init__(self, pandas_object): # noqa: E999
263 ...
264
265 For consistency with pandas methods, you should raise an ``AttributeError``
266 if the data passed to your accessor has an incorrect dtype.
267
268 >>> pd.Series(['a', 'b']).dt
269 Traceback (most recent call last):
270 ...
271 AttributeError: Can only use .dt accessor with datetimelike values
272
273 Examples
274 --------
275 In your library code::
276
277 import pandas as pd
278
279 @pd.api.extensions.register_dataframe_accessor("geo")
280 class GeoAccessor:
281 def __init__(self, pandas_obj):
282 self._obj = pandas_obj
283
284 @property
285 def center(self):
286 # return the geographic center point of this DataFrame
287 lat = self._obj.latitude
288 lon = self._obj.longitude
289 return (float(lon.mean()), float(lat.mean()))
290
291 def plot(self):
292 # plot this array's data on a map, e.g., using Cartopy
293 pass
294
295 Back in an interactive IPython session:
296
297 .. code-block:: ipython
298
299 In [1]: ds = pd.DataFrame({{"longitude": np.linspace(0, 10),
300 ...: "latitude": np.linspace(0, 20)}})
301 In [2]: ds.geo.center
302 Out[2]: (5.0, 10.0)
303 In [3]: ds.geo.plot() # plots data on a map
304 """
305
306 def decorator(accessor):
307 if hasattr(cls, name):
308 warnings.warn(
309 f"registration of accessor {repr(accessor)} under name "
310 f"{repr(name)} for type {repr(cls)} is overriding a preexisting "
311 f"attribute with the same name.",
312 UserWarning,
313 stacklevel=find_stack_level(),
314 )
315 setattr(cls, name, CachedAccessor(name, accessor))
316 cls._accessors.add(name)
317 return accessor
318
319 return decorator
320
321
322@doc(_register_accessor, klass="DataFrame")
323def register_dataframe_accessor(name):
324 from pandas import DataFrame
325
326 return _register_accessor(name, DataFrame)
327
328
329@doc(_register_accessor, klass="Series")
330def register_series_accessor(name):
331 from pandas import Series
332
333 return _register_accessor(name, Series)
334
335
336@doc(_register_accessor, klass="Index")
337def register_index_accessor(name):
338 from pandas import Index
339
340 return _register_accessor(name, Index)