Skip to content

Commit 209cc25

Browse files
authored
Merge pull request #28 from pyjanitor-devs/pyjviz-callbacks
addition of method_call_ctx_factory
2 parents 2825cf9 + b726d44 commit 209cc25

File tree

6 files changed

+228
-19
lines changed

6 files changed

+228
-19
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717
rev: 1.5.0
1818
hooks:
1919
- id: interrogate
20-
args: [-c, pyproject.toml]
20+
args: [-c, pyproject.toml, -vv]
2121
- repo: https://github.com/terrencepreilly/darglint
2222
rev: v1.8.1
2323
hooks:

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ If you have a feature request, please open an issue or submit a PR!
147147

148148
## TL;DR
149149

150-
Pandas 0.23 introduced a simpler API for [extending Pandas](https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extending-pandas). This API provided two key decorators, `register_dataframe_accessor` and `register_series_accessor`, that enable users to register **accessors** with Pandas DataFrames and Series.
150+
Pandas 0.23 introduced a simpler API for [extending Pandas](https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extending-pandas). This API provided two key decorators, `register_dataframe_accessor` and `register_series_accessor`, that enable users to register **accessors** with Pandas DataFrames and Series.
151151

152-
Pandas Flavor originated as a library to backport these decorators to older versions of Pandas (<0.23). While doing the backporting, it became clear that registering **methods** directly to Pandas objects might be a desired feature as well.[*](#footnote)
152+
Pandas Flavor originated as a library to backport these decorators to older versions of Pandas (<0.23). While doing the backporting, it became clear that registering **methods** directly to Pandas objects might be a desired feature as well.[*](#footnote)
153153

154-
<a name="footnote">*</a>*It is likely that Pandas deliberately chose not implement to this feature. If everyone starts monkeypatching DataFrames with their custom methods, it could lead to confusion in the Pandas community. The preferred Pandas approach is to namespace your methods by registering an accessor that contains your custom methods.*
154+
<a name="footnote">*</a>*It is likely that Pandas deliberately chose not implement to this feature. If everyone starts monkeypatching DataFrames with their custom methods, it could lead to confusion in the Pandas community. The preferred Pandas approach is to namespace your methods by registering an accessor that contains your custom methods.*
155155

156156
**So how does method registration work?**
157157

pandas_flavor/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Version number."""
2-
__version__ = "0.3.0"
2+
__version__ = "0.4.0"

pandas_flavor/register.py

Lines changed: 171 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,160 @@
11
from functools import wraps
2-
from pandas.api.extensions import register_series_accessor, register_dataframe_accessor
2+
from pandas.api.extensions import (
3+
register_series_accessor,
4+
register_dataframe_accessor,
5+
)
6+
import inspect
7+
8+
method_call_ctx_factory = None
9+
10+
11+
def handle_pandas_extension_call(method, method_signature, obj, args, kwargs):
12+
"""Handle pandas extension call.
13+
14+
This function is called when the user calls
15+
a pandas DataFrame object's method.
16+
The pandas extension mechanism passes args and kwargs
17+
of the original method call as it is applied to obj.
18+
19+
Our implementation uses the global variable `method_call_ctx_factory`.
20+
21+
`method_call_ctx_factory` can be either None or an abstract class.
22+
23+
When `method_call_ctx_factory` is None,
24+
the implementation calls the registered method
25+
with unmodified args and kwargs and returns underlying method result.
26+
27+
When `method_call_ctx_factory` is not None,
28+
`method_call_ctx_factory` is expected to refer to
29+
the function to create the context object.
30+
The context object will be used to process
31+
inputs and outputs of `method` calls.
32+
It is also possible that
33+
the context object method `handle_start_method_call`
34+
will modify original args and kwargs before `method` call.
35+
36+
`method_call_ctx_factory` is a function
37+
that should have the following signature:
38+
39+
`f(method_name: str, args: list, kwargs: dict) -> MethodCallCtx`
40+
41+
42+
MethodCallCtx is an abstract class:
43+
class MethodCallCtx(abc.ABC):
44+
45+
@abstractmethod
46+
def __enter__(self) -> None:
47+
raise NotImplemented
48+
49+
@abstractmethod
50+
def __exit__(self, exc_type, exc_value, traceback) -> None:
51+
raise NotImplemented
52+
53+
@abstractmethod
54+
def handle_start_method_call(self, method_name: str, method_signature: inspect.Signature, method_args: list, method_kwargs: dict) -> tuple(list, dict):
55+
raise NotImplemented
56+
57+
@abstractmethod
58+
def handle_end_method_call(self, ret: object) -> None:
59+
raise NotImplemented
60+
61+
62+
Args:
63+
method (callable): method object as registered by decorator
64+
register_dataframe_method (or register_series_method)
65+
method_signature: signature of method as returned by inspect.signature
66+
obj: Dataframe or Series
67+
args: The arguments to pass to the registered method.
68+
kwargs: The keyword arguments to pass to the registered method.
69+
70+
Returns:
71+
object`: The result of calling of the method.
72+
""" # noqa: E501
73+
74+
global method_call_ctx_factory
75+
with method_call_ctx_factory(
76+
method.__name__, args, kwargs
77+
) as method_call_ctx:
78+
if method_call_ctx is None: # nullcontext __enter__ returns None
79+
ret = method(obj, *args, **kwargs)
80+
else:
81+
all_args = tuple([obj] + list(args))
82+
(new_args, new_kwargs,) = method_call_ctx.handle_start_method_call(
83+
method.__name__, method_signature, all_args, kwargs
84+
)
85+
args = new_args[1:]
86+
kwargs = new_kwargs
87+
88+
ret = method(obj, *args, **kwargs)
89+
90+
method_call_ctx.handle_end_method_call(ret)
91+
92+
return ret
393

494

595
def register_dataframe_method(method):
696
"""Register a function as a method attached to the Pandas DataFrame.
797
8-
Example
9-
-------
10-
11-
.. code-block:: python
98+
Example:
1299
13100
@register_dataframe_method
14101
def print_column(df, col):
15102
'''Print the dataframe column given'''
16103
print(df[col])
104+
105+
Args:
106+
method (callable): callable to register as a dataframe method.
107+
108+
Returns:
109+
callable: The original method.
17110
"""
18111

112+
method_signature = inspect.signature(method)
113+
19114
def inner(*args, **kwargs):
115+
"""Inner function to register the method.
116+
117+
This function is called when the user
118+
decorates a function with register_dataframe_method.
119+
120+
Args:
121+
*args: The arguments to pass to the registered method.
122+
**kwargs: The keyword arguments to pass to the registered method.
123+
124+
Returns:
125+
method: The original method.
126+
"""
127+
20128
class AccessorMethod(object):
129+
"""DataFrame Accessor method class."""
130+
21131
def __init__(self, pandas_obj):
132+
"""Initialize the accessor method class.
133+
134+
Args:
135+
pandas_obj (pandas.DataFrame): The pandas DataFrame object.
136+
"""
22137
self._obj = pandas_obj
23138

24139
@wraps(method)
25140
def __call__(self, *args, **kwargs):
26-
return method(self._obj, *args, **kwargs)
141+
"""Call the accessor method.
142+
143+
Args:
144+
*args: The arguments to pass to the registered method.
145+
**kwargs: The keyword arguments to pass
146+
to the registered method.
147+
148+
Returns:
149+
object: The result of calling of the method.
150+
"""
151+
global method_call_ctx_factory
152+
if method_call_ctx_factory is None:
153+
return method(self._obj, *args, **kwargs)
154+
155+
return handle_pandas_extension_call(
156+
method, method_signature, self._obj, args, kwargs
157+
)
27158

28159
register_dataframe_accessor(method.__name__)(AccessorMethod)
29160

@@ -33,18 +164,50 @@ def __call__(self, *args, **kwargs):
33164

34165

35166
def register_series_method(method):
36-
"""Register a function as a method attached to the Pandas Series."""
167+
"""Register a function as a method attached to the Pandas Series.
168+
169+
Args:
170+
method (callable): callable to register as a series method.
171+
172+
Returns:
173+
callable: The original method.
174+
"""
175+
176+
method_signature = inspect.signature(method)
37177

38178
def inner(*args, **kwargs):
39179
class AccessorMethod(object):
180+
"""Series Accessor method class."""
181+
40182
__doc__ = method.__doc__
41183

42184
def __init__(self, pandas_obj):
185+
"""Initialize the accessor method class.
186+
187+
Args:
188+
pandas_obj (pandas.Series): The pandas Series object.
189+
"""
43190
self._obj = pandas_obj
44191

45192
@wraps(method)
46193
def __call__(self, *args, **kwargs):
47-
return method(self._obj, *args, **kwargs)
194+
"""Call the accessor method.
195+
196+
Args:
197+
*args: The arguments to pass to the registered method.
198+
**kwargs: The keyword arguments to pass
199+
to the registered method.
200+
201+
Returns:
202+
object: The result of calling of the method.
203+
"""
204+
global method_call_ctx_factory
205+
if method_call_ctx_factory is None:
206+
return method(self._obj, *args, **kwargs)
207+
208+
return handle_pandas_extension_call(
209+
method, method_signature, self._obj, args, kwargs
210+
)
48211

49212
register_series_accessor(method.__name__)(AccessorMethod)
50213

pandas_flavor/xarray.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""XArray support for pandas_flavor."""
12
from xarray import register_dataarray_accessor, register_dataset_accessor
23
from functools import wraps
34

@@ -6,29 +7,67 @@ def make_accessor_wrapper(method):
67
"""
78
Makes an XArray-compatible accessor to wrap a method to be added to an
89
xr.DataArray, xr.Dataset, or both.
9-
:param method: A method which takes an XArray object and needed parameters.
10-
:return: The result of calling ``method``.
10+
11+
Args:
12+
method: A method which takes an XArray object and needed parameters.
13+
14+
Returns:
15+
The result of calling ``method``.
1116
"""
1217

1318
class XRAccessor:
19+
"""XArray accessor for a method."""
20+
1421
def __init__(self, xr_obj):
22+
"""Initialize the accessor.
23+
24+
Args:
25+
xr_obj: The XArray object to which the accessor is attached.
26+
"""
1527
self._xr_obj = xr_obj
1628

1729
@wraps(method)
1830
def __call__(self, *args, **kwargs):
31+
"""Call the method.
32+
33+
Args:
34+
*args: Positional arguments to pass to the method.
35+
**kwargs: Keyword arguments to pass to the method.
36+
37+
Returns:
38+
The result of calling ``method``.
39+
"""
40+
1941
return method(self._xr_obj, *args, **kwargs)
2042

2143
return XRAccessor
2244

2345

2446
def register_xarray_dataarray_method(method: callable):
47+
"""Register a method on an XArray DataArray object.
48+
49+
Args:
50+
method: A method which takes an XArray object and needed parameters.
51+
52+
Returns:
53+
The method.
54+
"""
2555
accessor_wrapper = make_accessor_wrapper(method)
2656
register_dataarray_accessor(method.__name__)(accessor_wrapper)
2757

2858
return method
2959

3060

3161
def register_xarray_dataset_method(method: callable):
62+
"""Register a method on an XArray Dataset object.
63+
64+
Args:
65+
method: A method which takes an XArray object and needed parameters.
66+
67+
Returns:
68+
The method.
69+
"""
70+
3271
accessor_wrapper = make_accessor_wrapper(method)
3372
register_dataset_accessor(method.__name__)(accessor_wrapper)
3473

setup.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
# The rest you shouldn't have to touch too much :)
2525
# ------------------------------------------------
2626
# Except, perhaps the License and Trove Classifiers!
27-
# If you do change the License, remember to change the Trove Classifier for that!
27+
# If you do change the License,
28+
# remember to change the Trove Classifier for that!
2829

2930
here = os.path.abspath(os.path.dirname(__file__))
3031

3132
# Import the README and use it as the long-description.
32-
# Note: this will only work if 'README.rst' is present in your MANIFEST.in file!
33+
# Note: this will only work if 'README.rst'
34+
# is present in your MANIFEST.in file!
3335
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
3436
long_description = "\n" + f.read()
3537

@@ -46,8 +48,13 @@ class UploadCommand(Command):
4648
user_options = []
4749

4850
@staticmethod
49-
def status(s):
50-
"""Prints things in bold."""
51+
def status(s: str):
52+
"""Prints things in bold.
53+
54+
Args:
55+
s: The string to print.
56+
57+
"""
5158
print("\033[1m{0}\033[0m".format(s))
5259

5360
def initialize_options(self):

0 commit comments

Comments
 (0)