Skip to content

Commit dc67804

Browse files
jbrockmendelJosiah Baker
authored and
Josiah Baker
committed
CLN: Define and pin GroupBy properties without exec (pandas-dev#28651)
1 parent 5ec718a commit dc67804

File tree

3 files changed

+51
-41
lines changed

3 files changed

+51
-41
lines changed

ci/code_checks.sh

+5-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then
125125
# invgrep -R --include="*.py*" -E "from numpy import nan " pandas # GH#24822 not yet implemented since the offending imports have not all been removed
126126
RET=$(($RET + $?)) ; echo $MSG "DONE"
127127

128+
MSG='Check for use of exec' ; echo $MSG
129+
invgrep -R --include="*.py*" -E "[^a-zA-Z0-9_]exec\(" pandas
130+
RET=$(($RET + $?)) ; echo $MSG "DONE"
131+
128132
MSG='Check for pytest warns' ; echo $MSG
129133
invgrep -r -E --include '*.py' 'pytest\.warns' pandas/tests/
130134
RET=$(($RET + $?)) ; echo $MSG "DONE"
@@ -184,7 +188,7 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then
184188
invgrep -R --include="*.rst" ".. ipython ::" doc/source
185189
RET=$(($RET + $?)) ; echo $MSG "DONE"
186190

187-
MSG='Check that no file in the repo contains tailing whitespaces' ; echo $MSG
191+
MSG='Check that no file in the repo contains trailing whitespaces' ; echo $MSG
188192
set -o pipefail
189193
if [[ "$AZURE" == "true" ]]; then
190194
# we exclude all c/cpp files as the c/cpp files of pandas code base are tested when Linting .c and .h files

pandas/core/groupby/generic.py

+46-38
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from functools import partial
1212
from textwrap import dedent
1313
import typing
14-
from typing import Any, Callable, FrozenSet, Iterator, Sequence, Type, Union
14+
from typing import Any, Callable, FrozenSet, Sequence, Type, Union
1515
import warnings
1616

1717
import numpy as np
@@ -70,47 +70,63 @@
7070
ScalarResult = typing.TypeVar("ScalarResult")
7171

7272

73-
def whitelist_method_generator(
74-
base_class: Type[GroupBy], klass: Type[FrameOrSeries], whitelist: FrozenSet[str]
75-
) -> Iterator[str]:
73+
def generate_property(name: str, klass: Type[FrameOrSeries]):
7674
"""
77-
Yields all GroupBy member defs for DataFrame/Series names in whitelist.
75+
Create a property for a GroupBy subclass to dispatch to DataFrame/Series.
76+
77+
Parameters
78+
----------
79+
name : str
80+
klass : {DataFrame, Series}
81+
82+
Returns
83+
-------
84+
property
85+
"""
86+
87+
def prop(self):
88+
return self._make_wrapper(name)
89+
90+
parent_method = getattr(klass, name)
91+
prop.__doc__ = parent_method.__doc__ or ""
92+
prop.__name__ = name
93+
return property(prop)
94+
95+
96+
def pin_whitelisted_properties(klass: Type[FrameOrSeries], whitelist: FrozenSet[str]):
97+
"""
98+
Create GroupBy member defs for DataFrame/Series names in a whitelist.
7899
79100
Parameters
80101
----------
81-
base_class : Groupby class
82-
base class
83102
klass : DataFrame or Series class
84103
class where members are defined.
85-
whitelist : frozenset
104+
whitelist : frozenset[str]
86105
Set of names of klass methods to be constructed
87106
88107
Returns
89108
-------
90-
The generator yields a sequence of strings, each suitable for exec'ing,
91-
that define implementations of the named methods for DataFrameGroupBy
92-
or SeriesGroupBy.
109+
class decorator
93110
111+
Notes
112+
-----
94113
Since we don't want to override methods explicitly defined in the
95114
base class, any such name is skipped.
96115
"""
97-
property_wrapper_template = """@property
98-
def %(name)s(self) :
99-
\"""%(doc)s\"""
100-
return self.__getattr__('%(name)s')"""
101-
102-
for name in whitelist:
103-
# don't override anything that was explicitly defined
104-
# in the base class
105-
if hasattr(base_class, name):
106-
continue
107-
# ugly, but we need the name string itself in the method.
108-
f = getattr(klass, name)
109-
doc = f.__doc__
110-
doc = doc if type(doc) == str else ""
111-
wrapper_template = property_wrapper_template
112-
params = {"name": name, "doc": doc}
113-
yield wrapper_template % params
116+
117+
def pinner(cls):
118+
for name in whitelist:
119+
if hasattr(cls, name):
120+
# don't override anything that was explicitly defined
121+
# in the base class
122+
continue
123+
124+
prop = generate_property(name, klass)
125+
setattr(cls, name, prop)
126+
127+
return cls
128+
129+
return pinner
114130

115131

116132
class NDFrameGroupBy(GroupBy):
@@ -747,13 +763,9 @@ def filter(self, func, dropna=True, *args, **kwargs):
747763
return self._apply_filter(indices, dropna)
748764

749765

766+
@pin_whitelisted_properties(Series, base.series_apply_whitelist)
750767
class SeriesGroupBy(GroupBy):
751-
#
752-
# Make class defs of attributes on SeriesGroupBy whitelist
753-
754768
_apply_whitelist = base.series_apply_whitelist
755-
for _def_str in whitelist_method_generator(GroupBy, Series, _apply_whitelist):
756-
exec(_def_str)
757769

758770
@property
759771
def _selection_name(self):
@@ -1368,15 +1380,11 @@ def pct_change(self, periods=1, fill_method="pad", limit=None, freq=None):
13681380
return (filled / shifted) - 1
13691381

13701382

1383+
@pin_whitelisted_properties(DataFrame, base.dataframe_apply_whitelist)
13711384
class DataFrameGroupBy(NDFrameGroupBy):
13721385

13731386
_apply_whitelist = base.dataframe_apply_whitelist
13741387

1375-
#
1376-
# Make class defs of attributes on DataFrameGroupBy whitelist.
1377-
for _def_str in whitelist_method_generator(GroupBy, DataFrame, _apply_whitelist):
1378-
exec(_def_str)
1379-
13801388
_block_agg_axis = 1
13811389

13821390
_agg_see_also_doc = dedent(

pandas/core/groupby/groupby.py

-2
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,6 @@ def __getattr__(self, attr):
562562
return object.__getattribute__(self, attr)
563563
if attr in self.obj:
564564
return self[attr]
565-
if hasattr(self.obj, attr):
566-
return self._make_wrapper(attr)
567565

568566
raise AttributeError(
569567
"%r object has no attribute %r" % (type(self).__name__, attr)

0 commit comments

Comments
 (0)