From f8c4813dcbabbd7f797d3987af0a3d71e26560a0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 1 Oct 2018 08:50:36 -0700 Subject: [PATCH 1/2] Support passing function to Appender --- pandas/tests/util/test_util.py | 16 +++++++++++++++- pandas/util/_decorators.py | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pandas/tests/util/test_util.py b/pandas/tests/util/test_util.py index 6552655110557..e00e866e5bb18 100644 --- a/pandas/tests/util/test_util.py +++ b/pandas/tests/util/test_util.py @@ -10,7 +10,7 @@ from pandas.compat import intern, PY3 import pandas.core.common as com from pandas.util._move import move_into_mutable_buffer, BadMove, stolenbuf -from pandas.util._decorators import deprecate_kwarg, make_signature +from pandas.util._decorators import deprecate_kwarg, make_signature, Appender from pandas.util._validators import (validate_args, validate_kwargs, validate_args_and_kwargs, validate_bool_kwarg) @@ -531,3 +531,17 @@ def test_safe_import(monkeypatch): monkeypatch.setitem(sys.modules, mod_name, mod) assert not td.safe_import(mod_name, min_version="2.0") assert td.safe_import(mod_name, min_version="1.0") + + +class TestAppender(object): + def test_pass_callable(self): + + def func(): + """foo""" + return + + @Appender(func) + def wrapped(): + return + + assert wrapped.__doc__ == "foo" diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 82cd44113cb25..2644d5bb453d3 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -290,6 +290,12 @@ def my_dog(has='fleas'): """ def __init__(self, addendum, join='', indents=0): + if callable(addendum): + # allow for passing @Appender(func) instead of + # @Appender(func.__doc__), both more succinct and helpful when + # -oo optimization strips docstrings + addendum = addendum.__doc__ or '' + if indents > 0: self.addendum = indent(addendum, indents=indents) else: From 6b4103a8ef429ff3fa9190d5d2c5a0dcf86951ea Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 1 Oct 2018 10:12:35 -0700 Subject: [PATCH 2/2] Support appending/substituting class docstrings --- pandas/tests/util/test_util.py | 28 ++++++++++++++++++++++- pandas/util/_decorators.py | 42 +++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/pandas/tests/util/test_util.py b/pandas/tests/util/test_util.py index e00e866e5bb18..e99ecc489b2b8 100644 --- a/pandas/tests/util/test_util.py +++ b/pandas/tests/util/test_util.py @@ -10,7 +10,8 @@ from pandas.compat import intern, PY3 import pandas.core.common as com from pandas.util._move import move_into_mutable_buffer, BadMove, stolenbuf -from pandas.util._decorators import deprecate_kwarg, make_signature, Appender +from pandas.util._decorators import (deprecate_kwarg, make_signature, + Appender, Substitution) from pandas.util._validators import (validate_args, validate_kwargs, validate_args_and_kwargs, validate_bool_kwarg) @@ -535,6 +536,7 @@ def test_safe_import(monkeypatch): class TestAppender(object): def test_pass_callable(self): + # GH#22927 def func(): """foo""" @@ -545,3 +547,27 @@ def wrapped(): return assert wrapped.__doc__ == "foo" + + def test_append_class(self): + # GH#22927 + + @Appender("bar") + class cls(object): + pass + + assert cls.__doc__ == "bar" + assert cls.__name__ == "cls" + assert cls.__module__ == "pandas.tests.util.test_util" + + +class TestSubstitution(object): + def test_substitute_class(self): + # GH#22927 + + @Substitution(name="Bond, James Bond") + class cls(object): + """%(name)s""" + + assert cls.__doc__ == "Bond, James Bond" + assert cls.__name__ == "cls" + assert cls.__module__ == "pandas.tests.util.test_util" diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 2644d5bb453d3..70016f5923acb 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -244,8 +244,8 @@ def __init__(self, *args, **kwargs): self.params = args or kwargs def __call__(self, func): - func.__doc__ = func.__doc__ and func.__doc__ % self.params - return func + new_doc = func.__doc__ and func.__doc__ % self.params + return _set_docstring(func, new_doc) def update(self, *args, **kwargs): """ @@ -303,11 +303,41 @@ def __init__(self, addendum, join='', indents=0): self.join = join def __call__(self, func): - func.__doc__ = func.__doc__ if func.__doc__ else '' + doc = func.__doc__ if func.__doc__ else '' self.addendum = self.addendum if self.addendum else '' - docitems = [func.__doc__, self.addendum] - func.__doc__ = dedent(self.join.join(docitems)) - return func + docitems = [doc, self.addendum] + new_doc = dedent(self.join.join(docitems)) + + return _set_docstring(func, new_doc) + + +def _set_docstring(obj, docstring): + """ + Set the docstring for the given function or class + + Parameters + ---------- + obj : function, method, class + docstring : str + + Returns + ------- + same type as obj + """ + if isinstance(obj, type): + # i.e. decorating a class, for which docstrings can not be edited + + class Wrapped(obj): + __doc__ = docstring + + Wrapped.__name__ = obj.__name__ + Wrapped.__module__ = obj.__module__ + # TODO: will this induce a perf penalty in MRO lookups? + return Wrapped + + else: + obj.__doc__ = docstring + return obj def indent(text, indents=1):