Skip to content

CLN: Define and pin GroupBy properties without exec #28651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ci/code_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then
# invgrep -R --include="*.py*" -E "from numpy import nan " pandas # GH#24822 not yet implemented since the offending imports have not all been removed
RET=$(($RET + $?)) ; echo $MSG "DONE"

MSG='Check for use of exec' ; echo $MSG
invgrep -R --include="*.py*" -E "[^a-zA-Z0-9_]exec\(" pandas
RET=$(($RET + $?)) ; echo $MSG "DONE"

MSG='Check for pytest warns' ; echo $MSG
invgrep -r -E --include '*.py' 'pytest\.warns' pandas/tests/
RET=$(($RET + $?)) ; echo $MSG "DONE"
Expand Down Expand Up @@ -184,7 +188,7 @@ if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then
invgrep -R --include="*.rst" ".. ipython ::" doc/source
RET=$(($RET + $?)) ; echo $MSG "DONE"

MSG='Check that no file in the repo contains tailing whitespaces' ; echo $MSG
MSG='Check that no file in the repo contains trailing whitespaces' ; echo $MSG
set -o pipefail
if [[ "$AZURE" == "true" ]]; then
# we exclude all c/cpp files as the c/cpp files of pandas code base are tested when Linting .c and .h files
Expand Down
84 changes: 46 additions & 38 deletions pandas/core/groupby/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from functools import partial
from textwrap import dedent
import typing
from typing import Any, Callable, FrozenSet, Iterator, Sequence, Type, Union
from typing import Any, Callable, FrozenSet, Sequence, Type, Union
import warnings

import numpy as np
Expand Down Expand Up @@ -70,47 +70,63 @@
ScalarResult = typing.TypeVar("ScalarResult")


def whitelist_method_generator(
base_class: Type[GroupBy], klass: Type[FrameOrSeries], whitelist: FrozenSet[str]
) -> Iterator[str]:
def generate_property(name: str, klass: Type[FrameOrSeries]):
"""
Yields all GroupBy member defs for DataFrame/Series names in whitelist.
Create a property for a GroupBy subclass to dispatch to DataFrame/Series.

Parameters
----------
name : str
klass : {DataFrame, Series}

Returns
-------
property
"""

def prop(self):
return self._make_wrapper(name)

parent_method = getattr(klass, name)
prop.__doc__ = parent_method.__doc__ or ""
prop.__name__ = name
return property(prop)


def pin_whitelisted_properties(klass: Type[FrameOrSeries], whitelist: FrozenSet[str]):
"""
Create GroupBy member defs for DataFrame/Series names in a whitelist.

Parameters
----------
base_class : Groupby class
base class
klass : DataFrame or Series class
class where members are defined.
whitelist : frozenset
whitelist : frozenset[str]
Set of names of klass methods to be constructed

Returns
-------
The generator yields a sequence of strings, each suitable for exec'ing,
that define implementations of the named methods for DataFrameGroupBy
or SeriesGroupBy.
class decorator

Notes
-----
Since we don't want to override methods explicitly defined in the
base class, any such name is skipped.
"""
property_wrapper_template = """@property
def %(name)s(self) :
\"""%(doc)s\"""
return self.__getattr__('%(name)s')"""

for name in whitelist:
# don't override anything that was explicitly defined
# in the base class
if hasattr(base_class, name):
continue
# ugly, but we need the name string itself in the method.
f = getattr(klass, name)
doc = f.__doc__
doc = doc if type(doc) == str else ""
wrapper_template = property_wrapper_template
params = {"name": name, "doc": doc}
yield wrapper_template % params

def pinner(cls):
for name in whitelist:
if hasattr(cls, name):
# don't override anything that was explicitly defined
# in the base class
continue

prop = generate_property(name, klass)
setattr(cls, name, prop)

return cls

return pinner


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


@pin_whitelisted_properties(Series, base.series_apply_whitelist)
class SeriesGroupBy(GroupBy):
#
# Make class defs of attributes on SeriesGroupBy whitelist

_apply_whitelist = base.series_apply_whitelist
for _def_str in whitelist_method_generator(GroupBy, Series, _apply_whitelist):
exec(_def_str)

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


@pin_whitelisted_properties(DataFrame, base.dataframe_apply_whitelist)
class DataFrameGroupBy(NDFrameGroupBy):

_apply_whitelist = base.dataframe_apply_whitelist

#
# Make class defs of attributes on DataFrameGroupBy whitelist.
for _def_str in whitelist_method_generator(GroupBy, DataFrame, _apply_whitelist):
exec(_def_str)

_block_agg_axis = 1

_agg_see_also_doc = dedent(
Expand Down
2 changes: 0 additions & 2 deletions pandas/core/groupby/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,6 @@ def __getattr__(self, attr):
return object.__getattribute__(self, attr)
if attr in self.obj:
return self[attr]
if hasattr(self.obj, attr):
return self._make_wrapper(attr)

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