Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/accessor.py: 66%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
3accessor.py contains base classes for implementing accessor properties
4that can be mixed into or pinned onto other pandas classes.
6"""
8from __future__ import annotations
10import functools
11from typing import (
12 TYPE_CHECKING,
13 final,
14)
15import warnings
17from pandas.util._decorators import (
18 set_module,
19)
20from pandas.util._exceptions import find_stack_level
22if TYPE_CHECKING:
23 from collections.abc import Callable
25 from pandas._typing import TypeT
27 from pandas import Index
28 from pandas.core.generic import NDFrame
31class DirNamesMixin:
32 _accessors: set[str] = set()
33 _hidden_attrs: frozenset[str] = frozenset()
35 @final
36 def _dir_deletions(self) -> set[str]:
37 """
38 Delete unwanted __dir__ for this object.
39 """
40 return self._accessors | self._hidden_attrs
42 def _dir_additions(self) -> set[str]:
43 """
44 Add additional __dir__ for this object.
45 """
46 return {accessor for accessor in self._accessors if hasattr(self, accessor)}
48 def __dir__(self) -> list[str]:
49 """
50 Provide method name lookup and completion.
52 Notes
53 -----
54 Only provide 'public' methods.
55 """
56 rv = set(super().__dir__())
57 rv = (rv - self._dir_deletions()) | self._dir_additions()
58 return sorted(rv)
61class PandasDelegate:
62 """
63 Abstract base class for delegating methods/properties.
64 """
66 def _delegate_property_get(self, name: str, *args, **kwargs):
67 raise TypeError(f"You cannot access the property {name}")
69 def _delegate_property_set(self, name: str, value, *args, **kwargs) -> None:
70 raise TypeError(f"The property {name} cannot be set")
72 def _delegate_method(self, name: str, *args, **kwargs):
73 raise TypeError(f"You cannot call method {name}")
75 @classmethod
76 def _add_delegate_accessors(
77 cls,
78 delegate,
79 accessors: list[str],
80 typ: str,
81 overwrite: bool = False,
82 accessor_mapping: Callable[[str], str] = lambda x: x,
83 raise_on_missing: bool = True,
84 ) -> None:
85 """
86 Add accessors to cls from the delegate class.
88 Parameters
89 ----------
90 cls
91 Class to add the methods/properties to.
92 delegate
93 Class to get methods/properties and docstrings.
94 accessors : list of str
95 List of accessors to add.
96 typ : {'property', 'method'}
97 overwrite : bool, default False
98 Overwrite the method/property in the target class if it exists.
99 accessor_mapping: Callable, default lambda x: x
100 Callable to map the delegate's function to the cls' function.
101 raise_on_missing: bool, default True
102 Raise if an accessor does not exist on delegate.
103 False skips the missing accessor.
104 """
106 def _create_delegator_property(name: str):
107 def _getter(self):
108 return self._delegate_property_get(name)
110 def _setter(self, new_values):
111 return self._delegate_property_set(name, new_values)
113 _getter.__name__ = name
114 _setter.__name__ = name
116 return property(
117 fget=_getter,
118 fset=_setter,
119 doc=getattr(delegate, accessor_mapping(name)).__doc__,
120 )
122 def _create_delegator_method(name: str):
123 method = getattr(delegate, accessor_mapping(name))
125 @functools.wraps(method)
126 def f(self, *args, **kwargs):
127 return self._delegate_method(name, *args, **kwargs)
129 return f
131 for name in accessors:
132 if (
133 not raise_on_missing
134 and getattr(delegate, accessor_mapping(name), None) is None
135 ):
136 continue
138 if typ == "property":
139 f = _create_delegator_property(name)
140 else:
141 f = _create_delegator_method(name)
143 # don't overwrite existing methods/properties
144 if overwrite or not hasattr(cls, name):
145 setattr(cls, name, f)
148def delegate_names(
149 delegate,
150 accessors: list[str],
151 typ: str,
152 overwrite: bool = False,
153 accessor_mapping: Callable[[str], str] = lambda x: x,
154 raise_on_missing: bool = True,
155):
156 """
157 Add delegated names to a class using a class decorator. This provides
158 an alternative usage to directly calling `_add_delegate_accessors`
159 below a class definition.
161 Parameters
162 ----------
163 delegate : object
164 The class to get methods/properties & docstrings.
165 accessors : Sequence[str]
166 List of accessor to add.
167 typ : {'property', 'method'}
168 overwrite : bool, default False
169 Overwrite the method/property in the target class if it exists.
170 accessor_mapping: Callable, default lambda x: x
171 Callable to map the delegate's function to the cls' function.
172 raise_on_missing: bool, default True
173 Raise if an accessor does not exist on delegate.
174 False skips the missing accessor.
176 Returns
177 -------
178 callable
179 A class decorator.
181 Examples
182 --------
183 @delegate_names(Categorical, ["categories", "ordered"], "property")
184 class CategoricalAccessor(PandasDelegate):
185 [...]
186 """
188 def add_delegate_accessors(cls):
189 cls._add_delegate_accessors(
190 delegate,
191 accessors,
192 typ,
193 overwrite=overwrite,
194 accessor_mapping=accessor_mapping,
195 raise_on_missing=raise_on_missing,
196 )
197 return cls
199 return add_delegate_accessors
202class Accessor:
203 """
204 Custom property-like object.
206 A descriptor for accessors.
208 Parameters
209 ----------
210 name : str
211 Namespace that will be accessed under, e.g. ``df.foo``.
212 accessor : cls
213 Class with the extension methods.
215 Notes
216 -----
217 For accessor, The class's __init__ method assumes that one of
218 ``Series``, ``DataFrame`` or ``Index`` as the
219 single argument ``data``.
220 """
222 def __init__(self, name: str, accessor) -> None:
223 self._name = name
224 self._accessor = accessor
226 def __get__(self, obj, cls):
227 if obj is None:
228 # we're accessing the attribute of the class, i.e., Dataset.geo
229 return self._accessor
230 return self._accessor(obj)
233# Alias kept for downstream libraries
234# TODO: Deprecate as name is now misleading
235CachedAccessor = Accessor
238def _register_accessor(
239 name: str, cls: type[NDFrame | Index]
240) -> Callable[[TypeT], TypeT]:
241 """
242 Register a custom accessor on objects.
244 Parameters
245 ----------
246 name : str
247 Name under which the accessor should be registered. A warning is issued
248 if this name conflicts with a preexisting attribute.
250 Returns
251 -------
252 callable
253 A class decorator.
255 See Also
256 --------
257 register_dataframe_accessor : Register a custom accessor on DataFrame objects.
258 register_series_accessor : Register a custom accessor on Series objects.
259 register_index_accessor : Register a custom accessor on Index objects.
261 Notes
262 -----
263 This function allows you to register a custom-defined accessor class
264 for pandas objects (DataFrame, Series, or Index).
265 The requirements for the accessor class are as follows:
267 * Must contain an init method that:
269 * accepts a single object
271 * raises an AttributeError if the object does not have correctly
272 matching inputs for the accessor
274 * Must contain a method for each access pattern.
276 * The methods should be able to take any argument signature.
278 * Accessible using the @property decorator if no additional arguments are
279 needed.
281 """
283 def decorator(accessor: TypeT) -> TypeT:
284 if hasattr(cls, name):
285 warnings.warn(
286 f"registration of accessor {accessor!r} under name "
287 f"{name!r} for type {cls!r} is overriding a preexisting "
288 f"attribute with the same name.",
289 UserWarning,
290 stacklevel=find_stack_level(),
291 )
292 setattr(cls, name, Accessor(name, accessor))
293 cls._accessors.add(name)
294 return accessor
296 return decorator
299_register_df_examples = """
300An accessor that only accepts integers could
301have a class defined like this:
303>>> @pd.api.extensions.register_dataframe_accessor("int_accessor")
304... class IntAccessor:
305... def __init__(self, pandas_obj):
306... if not all(pandas_obj[col].dtype == 'int64' for col in pandas_obj.columns):
307... raise AttributeError("All columns must contain integer values only")
308... self._obj = pandas_obj
309...
310... def sum(self):
311... return self._obj.sum()
312...
313>>> df = pd.DataFrame([[1, 2], ['x', 'y']])
314>>> df.int_accessor
315Traceback (most recent call last):
316...
317AttributeError: All columns must contain integer values only.
318>>> df = pd.DataFrame([[1, 2], [3, 4]])
319>>> df.int_accessor.sum()
3200 4
3211 6
322dtype: int64"""
325@set_module("pandas.api.extensions")
326def register_dataframe_accessor(name: str) -> Callable[[TypeT], TypeT]:
327 """
328 Register a custom accessor on DataFrame objects.
330 Parameters
331 ----------
332 name : str
333 Name under which the accessor should be registered. A warning is issued
334 if this name conflicts with a preexisting attribute.
336 Returns
337 -------
338 callable
339 A class decorator.
341 See Also
342 --------
343 register_dataframe_accessor : Register a custom accessor on DataFrame objects.
344 register_series_accessor : Register a custom accessor on Series objects.
345 register_index_accessor : Register a custom accessor on Index objects.
347 Notes
348 -----
349 This function allows you to register a custom-defined accessor class for DataFrame.
350 The requirements for the accessor class are as follows:
352 * Must contain an init method that:
354 * accepts a single DataFrame object
356 * raises an AttributeError if the DataFrame object does not have correctly
357 matching inputs for the accessor
359 * Must contain a method for each access pattern.
361 * The methods should be able to take any argument signature.
363 * Accessible using the @property decorator if no additional arguments are
364 needed.
366 Examples
367 --------
368 An accessor that only accepts integers could
369 have a class defined like this:
371 >>> @pd.api.extensions.register_dataframe_accessor("int_accessor")
372 ... class IntAccessor:
373 ... def __init__(self, pandas_obj):
374 ... if not all(
375 ... pandas_obj[col].dtype == "int64" for col in pandas_obj.columns
376 ... ):
377 ... raise AttributeError("All columns must contain integer values only")
378 ... self._obj = pandas_obj
379 ...
380 ... def sum(self):
381 ... return self._obj.sum()
382 >>> df = pd.DataFrame([[1, 2], ["x", "y"]])
383 >>> df.int_accessor
384 Traceback (most recent call last):
385 ...
386 AttributeError: All columns must contain integer values only.
387 >>> df = pd.DataFrame([[1, 2], [3, 4]])
388 >>> df.int_accessor.sum()
389 0 4
390 1 6
391 dtype: int64
392 """
393 from pandas import DataFrame
395 return _register_accessor(name, DataFrame)
398_register_series_examples = """
399An accessor that only accepts integers could
400have a class defined like this:
402>>> @pd.api.extensions.register_series_accessor("int_accessor")
403... class IntAccessor:
404... def __init__(self, pandas_obj):
405... if not pandas_obj.dtype == 'int64':
406... raise AttributeError("The series must contain integer data only")
407... self._obj = pandas_obj
408...
409... def sum(self):
410... return self._obj.sum()
411...
412>>> df = pd.Series([1, 2, 'x'])
413>>> df.int_accessor
414Traceback (most recent call last):
415...
416AttributeError: The series must contain integer data only.
417>>> df = pd.Series([1, 2, 3])
418>>> df.int_accessor.sum()
4196"""
422@set_module("pandas.api.extensions")
423def register_series_accessor(name: str) -> Callable[[TypeT], TypeT]:
424 """
425 Register a custom accessor on Series objects.
427 Parameters
428 ----------
429 name : str
430 Name under which the accessor should be registered. A warning is issued
431 if this name conflicts with a preexisting attribute.
433 Returns
434 -------
435 callable
436 A class decorator.
438 See Also
439 --------
440 register_dataframe_accessor : Register a custom accessor on DataFrame objects.
441 register_series_accessor : Register a custom accessor on Series objects.
442 register_index_accessor : Register a custom accessor on Index objects.
444 Notes
445 -----
446 This function allows you to register a custom-defined accessor class for Series.
447 The requirements for the accessor class are as follows:
449 * Must contain an init method that:
451 * accepts a single Series object
453 * raises an AttributeError if the Series object does not have correctly
454 matching inputs for the accessor
456 * Must contain a method for each access pattern.
458 * The methods should be able to take any argument signature.
460 * Accessible using the @property decorator if no additional arguments are
461 needed.
463 Examples
464 --------
465 An accessor that only accepts integers could
466 have a class defined like this:
468 >>> @pd.api.extensions.register_series_accessor("int_accessor")
469 ... class IntAccessor:
470 ... def __init__(self, pandas_obj):
471 ... if not pandas_obj.dtype == "int64":
472 ... raise AttributeError("The series must contain integer data only")
473 ... self._obj = pandas_obj
474 ...
475 ... def sum(self):
476 ... return self._obj.sum()
477 >>> df = pd.Series([1, 2, "x"])
478 >>> df.int_accessor
479 Traceback (most recent call last):
480 ...
481 AttributeError: The series must contain integer data only.
482 >>> df = pd.Series([1, 2, 3])
483 >>> df.int_accessor.sum()
484 6
485 """
486 from pandas import Series
488 return _register_accessor(name, Series)
491_register_index_examples = """
492An accessor that only accepts integers could
493have a class defined like this:
495>>> @pd.api.extensions.register_index_accessor("int_accessor")
496... class IntAccessor:
497... def __init__(self, pandas_obj):
498... if not all(isinstance(x, int) for x in pandas_obj):
499... raise AttributeError("The index must only be an integer value")
500... self._obj = pandas_obj
501...
502... def even(self):
503... return [x for x in self._obj if x % 2 == 0]
504>>> df = pd.DataFrame.from_dict(
505... {"row1": {"1": 1, "2": "a"}, "row2": {"1": 2, "2": "b"}}, orient="index"
506... )
507>>> df.index.int_accessor
508Traceback (most recent call last):
509...
510AttributeError: The index must only be an integer value.
511>>> df = pd.DataFrame(
512... {"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]}, index=[1, 2, 5, 8]
513... )
514>>> df.index.int_accessor.even()
515[2, 8]"""
518@set_module("pandas.api.extensions")
519def register_index_accessor(name: str) -> Callable[[TypeT], TypeT]:
520 """
521 Register a custom accessor on Index objects.
523 Parameters
524 ----------
525 name : str
526 Name under which the accessor should be registered. A warning is issued
527 if this name conflicts with a preexisting attribute.
529 Returns
530 -------
531 callable
532 A class decorator.
534 See Also
535 --------
536 register_dataframe_accessor : Register a custom accessor on DataFrame objects.
537 register_series_accessor : Register a custom accessor on Series objects.
538 register_index_accessor : Register a custom accessor on Index objects.
540 Notes
541 -----
542 This function allows you to register a custom-defined accessor class for Index.
543 The requirements for the accessor class are as follows:
545 * Must contain an init method that:
547 * accepts a single Index object
549 * raises an AttributeError if the Index object does not have correctly
550 matching inputs for the accessor
552 * Must contain a method for each access pattern.
554 * The methods should be able to take any argument signature.
556 * Accessible using the @property decorator if no additional arguments are
557 needed.
559 Examples
560 --------
561 An accessor that only accepts integers could
562 have a class defined like this:
564 >>> @pd.api.extensions.register_index_accessor("int_accessor")
565 ... class IntAccessor:
566 ... def __init__(self, pandas_obj):
567 ... if not all(isinstance(x, int) for x in pandas_obj):
568 ... raise AttributeError("The index must only be an integer value")
569 ... self._obj = pandas_obj
570 ...
571 ... def even(self):
572 ... return [x for x in self._obj if x % 2 == 0]
573 >>> df = pd.DataFrame.from_dict(
574 ... {"row1": {"1": 1, "2": "a"}, "row2": {"1": 2, "2": "b"}}, orient="index"
575 ... )
576 >>> df.index.int_accessor
577 Traceback (most recent call last):
578 ...
579 AttributeError: The index must only be an integer value.
580 >>> df = pd.DataFrame(
581 ... {"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]}, index=[1, 2, 5, 8]
582 ... )
583 >>> df.index.int_accessor.even()
584 [2, 8]
585 """
586 from pandas import Index
588 return _register_accessor(name, Index)