diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index ba4c2e168e0c4..24a2bf12fd0b5 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -15,6 +15,7 @@ Substitution, cache_readonly, deprecate_kwarg, + doc, ) from pandas.util._validators import validate_bool_kwarg, validate_fillna_kwargs @@ -1352,8 +1353,7 @@ def memory_usage(self, deep=False): """ return self._codes.nbytes + self.dtype.categories.memory_usage(deep=deep) - @Substitution(klass="Categorical") - @Appender(_shared_docs["searchsorted"]) + @doc(_shared_docs["searchsorted"], klass="Categorical") def searchsorted(self, value, side="left", sorter=None): # searchsorted is very performance sensitive. By converting codes # to same dtype as self.codes, we get much faster performance. diff --git a/pandas/core/base.py b/pandas/core/base.py index 478b83f538b7d..56cd49e040ec2 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -13,7 +13,7 @@ from pandas.compat import PYPY from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender, Substitution, cache_readonly, doc +from pandas.util._decorators import cache_readonly, doc from pandas.util._validators import validate_bool_kwarg from pandas.core.dtypes.cast import is_nested_object @@ -1429,13 +1429,13 @@ def factorize(self, sort=False, na_sentinel=-1): ] = """ Find indices where elements should be inserted to maintain order. - Find the indices into a sorted %(klass)s `self` such that, if the + Find the indices into a sorted {klass} `self` such that, if the corresponding elements in `value` were inserted before the indices, the order of `self` would be preserved. .. note:: - The %(klass)s *must* be monotonically sorted, otherwise + The {klass} *must* be monotonically sorted, otherwise wrong locations will likely be returned. Pandas does *not* check this for you. @@ -1443,7 +1443,7 @@ def factorize(self, sort=False, na_sentinel=-1): ---------- value : array_like Values to insert into `self`. - side : {'left', 'right'}, optional + side : {{'left', 'right'}}, optional If 'left', the index of the first suitable location found is given. If 'right', return the last such index. If there is no suitable index, return either 0 or N (where N is the length of `self`). @@ -1519,8 +1519,7 @@ def factorize(self, sort=False, na_sentinel=-1): 0 # wrong result, correct would be 1 """ - @Substitution(klass="Index") - @Appender(_shared_docs["searchsorted"]) + @doc(_shared_docs["searchsorted"], klass="Index") def searchsorted(self, value, side="left", sorter=None) -> np.ndarray: return algorithms.searchsorted(self._values, value, side=side, sorter=sorter) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 894e1d95a17bc..f5efde99d7950 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -11,7 +11,7 @@ from pandas._typing import Label from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender, cache_readonly +from pandas.util._decorators import Appender, cache_readonly, doc from pandas.core.dtypes.common import ( ensure_int64, @@ -31,7 +31,7 @@ from pandas.core import algorithms from pandas.core.arrays import DatetimeArray, PeriodArray, TimedeltaArray from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin -from pandas.core.base import _shared_docs +from pandas.core.base import IndexOpsMixin import pandas.core.indexes.base as ibase from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.extension import ( @@ -206,7 +206,7 @@ def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs): self, indices, axis, allow_fill, fill_value, **kwargs ) - @Appender(_shared_docs["searchsorted"]) + @doc(IndexOpsMixin.searchsorted, klass="Datetime-like Index") def searchsorted(self, value, side="left", sorter=None): if isinstance(value, str): raise TypeError( diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 9a671c7fc170a..3d86162d2a88c 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -5,7 +5,7 @@ from pandas._libs.indexing import _NDFrameIndexerBase from pandas._libs.lib import item_from_zerodim from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender +from pandas.util._decorators import doc from pandas.core.dtypes.common import ( is_integer, @@ -847,7 +847,7 @@ def _getbool_axis(self, key, axis: int): return self.obj._take_with_is_copy(inds, axis=axis) -@Appender(IndexingMixin.loc.__doc__) +@doc(IndexingMixin.loc) class _LocIndexer(_LocationIndexer): _takeable: bool = False _valid_types = ( @@ -859,7 +859,7 @@ class _LocIndexer(_LocationIndexer): # ------------------------------------------------------------------- # Key Checks - @Appender(_LocationIndexer._validate_key.__doc__) + @doc(_LocationIndexer._validate_key) def _validate_key(self, key, axis: int): # valid for a collection of labels (we check their presence later) @@ -1289,7 +1289,7 @@ def _validate_read_indexer( ) -@Appender(IndexingMixin.iloc.__doc__) +@doc(IndexingMixin.iloc) class _iLocIndexer(_LocationIndexer): _valid_types = ( "integer, integer slice (START point is INCLUDED, END " @@ -1998,7 +1998,7 @@ def __setitem__(self, key, value): self.obj._set_value(*key, value=value, takeable=self._takeable) -@Appender(IndexingMixin.at.__doc__) +@doc(IndexingMixin.at) class _AtIndexer(_ScalarAccessIndexer): _takeable = False @@ -2024,7 +2024,7 @@ def __getitem__(self, key): return obj.index._get_values_for_loc(obj, loc, key) -@Appender(IndexingMixin.iat.__doc__) +@doc(IndexingMixin.iat) class _iAtIndexer(_ScalarAccessIndexer): _takeable = True diff --git a/pandas/core/series.py b/pandas/core/series.py index 568e99622dd29..6c8b1c1085b18 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2443,8 +2443,7 @@ def __rmatmul__(self, other): """ return self.dot(np.transpose(other)) - @Substitution(klass="Series") - @Appender(base._shared_docs["searchsorted"]) + @doc(base.IndexOpsMixin.searchsorted, klass="Series") def searchsorted(self, value, side="left", sorter=None): return algorithms.searchsorted(self._values, value, side=side, sorter=sorter) diff --git a/pandas/tests/util/test_doc.py b/pandas/tests/util/test_doc.py index 7e5e24456b9a7..50859564e654f 100644 --- a/pandas/tests/util/test_doc.py +++ b/pandas/tests/util/test_doc.py @@ -14,13 +14,15 @@ def cumsum(whatever): @doc( cumsum, - """ - Examples - -------- + dedent( + """ + Examples + -------- - >>> cumavg([1, 2, 3]) - 2 - """, + >>> cumavg([1, 2, 3]) + 2 + """ + ), method="cumavg", operation="average", ) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index d854be062fcbb..7a804792174c7 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -250,9 +250,11 @@ def doc(*args: Union[str, Callable], **kwargs: str) -> Callable[[F], F]: A decorator take docstring templates, concatenate them and perform string substitution on it. - This decorator is robust even if func.__doc__ is None. This decorator will - add a variable "_docstr_template" to the wrapped function to save original - docstring template for potential usage. + This decorator will add a variable "_docstring_components" to the wrapped + function to keep track the original docstring template for potential usage. + If it should be consider as a template, it will be saved as a string. + Otherwise, it will be saved as callable, and later user __doc__ and dedent + to get docstring. Parameters ---------- @@ -268,17 +270,28 @@ def decorator(func: F) -> F: def wrapper(*args, **kwargs) -> Callable: return func(*args, **kwargs) - templates = [func.__doc__ if func.__doc__ else ""] + # collecting docstring and docstring templates + docstring_components: List[Union[str, Callable]] = [] + if func.__doc__: + docstring_components.append(dedent(func.__doc__)) + for arg in args: - if isinstance(arg, str): - templates.append(arg) - elif hasattr(arg, "_docstr_template"): - templates.append(arg._docstr_template) # type: ignore - elif arg.__doc__: - templates.append(arg.__doc__) - - wrapper._docstr_template = "".join(dedent(t) for t in templates) # type: ignore - wrapper.__doc__ = wrapper._docstr_template.format(**kwargs) # type: ignore + if hasattr(arg, "_docstring_components"): + docstring_components.extend(arg._docstring_components) # type: ignore + elif isinstance(arg, str) or arg.__doc__: + docstring_components.append(arg) + + # formatting templates and concatenating docstring + wrapper.__doc__ = "".join( + [ + arg.format(**kwargs) + if isinstance(arg, str) + else dedent(arg.__doc__ or "") + for arg in docstring_components + ] + ) + + wrapper._docstring_components = docstring_components # type: ignore return cast(F, wrapper)