diff --git a/pandas/tests/util/test_util.py b/pandas/tests/util/test_util.py index 6552655110557..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 +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) @@ -531,3 +532,42 @@ 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): + # GH#22927 + + def func(): + """foo""" + return + + @Appender(func) + 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 82cd44113cb25..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): """ @@ -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: @@ -297,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):