From 5911aa3ecbb6b925ffbd23d0c4da6f098be8d851 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Wed, 15 Jan 2020 20:38:55 -0500 Subject: [PATCH 01/14] add docstring decorator --- pandas/util/_decorators.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index d10d3a1f71fe6..2e8888807a85f 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -247,6 +247,36 @@ def wrapper(*args, **kwargs) -> Callable[..., Any]: return decorate +def doc(*args: Union[str, Callable], **kwargs: str) -> Callable: + """ + A decorator take docstring templates, concatenate them and perform string + substitution on it. + + This decorator should be robust even if func.__doc__ is None. + """ + + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> Callable: + return func(*args, **kwargs) + + templates = [func.__doc__ if func.__doc__ else ""] + 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 + + return wrapper + + return decorator + + # Substitution and Appender are derived from matplotlib.docstring (1.1.0) # module http://matplotlib.org/users/license.html From a04723f0c052c266ca02cf289a097bc3766db092 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Wed, 15 Jan 2020 21:05:27 -0500 Subject: [PATCH 02/14] use doc decorator for _shared_docs['factorize'] --- pandas/core/algorithms.py | 63 ++++++++++++++++++--------------------- pandas/core/base.py | 6 ++-- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 59256f6924b79..3e8f5cc84e739 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -10,7 +10,7 @@ from pandas._libs import Timestamp, algos, hashtable as htable, lib from pandas._libs.tslib import iNaT -from pandas.util._decorators import Appender, Substitution +from pandas.util._decorators import doc from pandas.core.dtypes.cast import ( construct_1d_object_array_from_listlike, @@ -480,9 +480,32 @@ def _factorize_array( return codes, uniques -_shared_docs[ - "factorize" -] = """ +@doc( + values=dedent( + """\ + values : sequence + A 1-D sequence. Sequences that aren't pandas objects are + coerced to ndarrays before factorization. + """ + ), + sort=dedent( + """\ + sort : bool, default False + Sort `uniques` and shuffle `codes` to maintain the + relationship. + """ + ), + size_hint=dedent( + """\ + size_hint : int, optional + Hint to the hashtable sizer. + """ + ), +) +def factorize( + values, sort: bool = False, na_sentinel: int = -1, size_hint: Optional[int] = None +) -> Tuple[np.ndarray, Union[np.ndarray, ABCIndex]]: + """ Encode the object as an enumerated type or categorical variable. This method is useful for obtaining a numeric representation of an @@ -492,10 +515,10 @@ def _factorize_array( Parameters ---------- - %(values)s%(sort)s + {values}{sort} na_sentinel : int, default -1 Value to mark "not found". - %(size_hint)s\ + {size_hint}\ Returns ------- @@ -573,34 +596,6 @@ def _factorize_array( >>> uniques Index(['a', 'c'], dtype='object') """ - - -@Substitution( - values=dedent( - """\ - values : sequence - A 1-D sequence. Sequences that aren't pandas objects are - coerced to ndarrays before factorization. - """ - ), - sort=dedent( - """\ - sort : bool, default False - Sort `uniques` and shuffle `codes` to maintain the - relationship. - """ - ), - size_hint=dedent( - """\ - size_hint : int, optional - Hint to the hashtable sizer. - """ - ), -) -@Appender(_shared_docs["factorize"]) -def factorize( - values, sort: bool = False, na_sentinel: int = -1, size_hint: Optional[int] = None -) -> Tuple[np.ndarray, Union[np.ndarray, ABCIndex]]: # Implementation notes: This method is responsible for 3 things # 1.) coercing data to array-like (ndarray, Index, extension array) # 2.) factorizing codes and uniques diff --git a/pandas/core/base.py b/pandas/core/base.py index 66d7cd59dcfa4..3a2aba3b42421 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -12,7 +12,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 +from pandas.util._decorators import Appender, Substitution, cache_readonly, doc from pandas.util._validators import validate_bool_kwarg from pandas.core.dtypes.cast import is_nested_object @@ -1389,7 +1389,8 @@ def memory_usage(self, deep=False): v += lib.memory_usage_of_objects(self.array) return v - @Substitution( + @doc( + algorithms.factorize, values="", order="", size_hint="", @@ -1401,7 +1402,6 @@ def memory_usage(self, deep=False): """ ), ) - @Appender(algorithms._shared_docs["factorize"]) def factorize(self, sort=False, na_sentinel=-1): return algorithms.factorize(self, sort=sort, na_sentinel=na_sentinel) From b96a68f471fe2df5fb8655a6d730f2d7b5e04376 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Thu, 16 Jan 2020 23:28:36 -0500 Subject: [PATCH 03/14] use doc decorator --- pandas/core/accessor.py | 161 ++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/pandas/core/accessor.py b/pandas/core/accessor.py index 3f1c7b1c049cf..cd6211034875c 100644 --- a/pandas/core/accessor.py +++ b/pandas/core/accessor.py @@ -7,7 +7,7 @@ from typing import FrozenSet, Set import warnings -from pandas.util._decorators import Appender +from pandas.util._decorators import doc class DirNamesMixin: @@ -193,98 +193,97 @@ def __get__(self, obj, cls): return accessor_obj +@doc(klass="", others="") def _register_accessor(name, cls): - def decorator(accessor): - if hasattr(cls, name): - warnings.warn( - f"registration of accessor {repr(accessor)} under name " - f"{repr(name)} for type {repr(cls)} is overriding a preexisting" - f"attribute with the same name.", - UserWarning, - stacklevel=2, - ) - setattr(cls, name, CachedAccessor(name, accessor)) - cls._accessors.add(name) - return accessor - - return decorator + """ + Register a custom accessor on {klass} objects. + Parameters + ---------- + name : str + Name under which the accessor should be registered. A warning is issued + if this name conflicts with a preexisting attribute. -_doc = """ -Register a custom accessor on %(klass)s objects. + Returns + ------- + callable + A class decorator. -Parameters ----------- -name : str - Name under which the accessor should be registered. A warning is issued - if this name conflicts with a preexisting attribute. + See Also + -------- + {others} -Returns -------- -callable - A class decorator. + Notes + ----- + When accessed, your accessor will be initialized with the pandas object + the user is interacting with. So the signature must be -See Also --------- -%(others)s + .. code-block:: python -Notes ------ -When accessed, your accessor will be initialized with the pandas object -the user is interacting with. So the signature must be + def __init__(self, pandas_object): # noqa: E999 + ... -.. code-block:: python + For consistency with pandas methods, you should raise an ``AttributeError`` + if the data passed to your accessor has an incorrect dtype. - def __init__(self, pandas_object): # noqa: E999 - ... + >>> pd.Series(['a', 'b']).dt + Traceback (most recent call last): + ... + AttributeError: Can only use .dt accessor with datetimelike values -For consistency with pandas methods, you should raise an ``AttributeError`` -if the data passed to your accessor has an incorrect dtype. + Examples + -------- ->>> pd.Series(['a', 'b']).dt -Traceback (most recent call last): -... -AttributeError: Can only use .dt accessor with datetimelike values + In your library code:: -Examples --------- + import pandas as pd -In your library code:: + @pd.api.extensions.register_dataframe_accessor("geo") + class GeoAccessor: + def __init__(self, pandas_obj): + self._obj = pandas_obj - import pandas as pd + @property + def center(self): + # return the geographic center point of this DataFrame + lat = self._obj.latitude + lon = self._obj.longitude + return (float(lon.mean()), float(lat.mean())) - @pd.api.extensions.register_dataframe_accessor("geo") - class GeoAccessor: - def __init__(self, pandas_obj): - self._obj = pandas_obj + def plot(self): + # plot this array's data on a map, e.g., using Cartopy + pass - @property - def center(self): - # return the geographic center point of this DataFrame - lat = self._obj.latitude - lon = self._obj.longitude - return (float(lon.mean()), float(lat.mean())) + Back in an interactive IPython session: - def plot(self): - # plot this array's data on a map, e.g., using Cartopy - pass + >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), + ... 'latitude': np.linspace(0, 20)}) + >>> ds.geo.center + (5.0, 10.0) + >>> ds.geo.plot() + # plots data on a map + """ -Back in an interactive IPython session: + def decorator(accessor): + if hasattr(cls, name): + warnings.warn( + f"registration of accessor {repr(accessor)} under name " + f"{repr(name)} for type {repr(cls)} is overriding a preexisting" + f"attribute with the same name.", + UserWarning, + stacklevel=2, + ) + setattr(cls, name, CachedAccessor(name, accessor)) + cls._accessors.add(name) + return accessor - >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), - ... 'latitude': np.linspace(0, 20)}) - >>> ds.geo.center - (5.0, 10.0) - >>> ds.geo.plot() - # plots data on a map -""" + return decorator -@Appender( - _doc - % dict( - klass="DataFrame", others=("register_series_accessor, register_index_accessor") - ) +@doc( + _register_accessor, + klass="DataFrame", + others="register_series_accessor, register_index_accessor", ) def register_dataframe_accessor(name): from pandas import DataFrame @@ -292,11 +291,10 @@ def register_dataframe_accessor(name): return _register_accessor(name, DataFrame) -@Appender( - _doc - % dict( - klass="Series", others=("register_dataframe_accessor, register_index_accessor") - ) +@doc( + _register_accessor, + klass="Series", + others="register_dataframe_accessor, register_index_accessor", ) def register_series_accessor(name): from pandas import Series @@ -304,11 +302,10 @@ def register_series_accessor(name): return _register_accessor(name, Series) -@Appender( - _doc - % dict( - klass="Index", others=("register_dataframe_accessor, register_series_accessor") - ) +@doc( + _register_accessor, + klass="Index", + others="register_dataframe_accessor, register_series_accessor", ) def register_index_accessor(name): from pandas import Index From 7755c3fdf2cfc001c1c0f761407d1165498db8e6 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Thu, 16 Jan 2020 23:49:40 -0500 Subject: [PATCH 04/14] fix docstring in _register_accessor --- pandas/core/accessor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/accessor.py b/pandas/core/accessor.py index cd6211034875c..747b05380eaa0 100644 --- a/pandas/core/accessor.py +++ b/pandas/core/accessor.py @@ -256,8 +256,8 @@ def plot(self): Back in an interactive IPython session: - >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10), - ... 'latitude': np.linspace(0, 20)}) + >>> ds = pd.DataFrame({{'longitude': np.linspace(0, 10), + ... 'latitude': np.linspace(0, 20)}}) >>> ds.geo.center (5.0, 10.0) >>> ds.geo.plot() From 4c5cd2801ca62d31405ec10921c1665a371ab734 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Fri, 17 Jan 2020 08:24:10 -0500 Subject: [PATCH 05/14] add test cases for doc decorator --- pandas/tests/util/test_doc.py | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pandas/tests/util/test_doc.py diff --git a/pandas/tests/util/test_doc.py b/pandas/tests/util/test_doc.py new file mode 100644 index 0000000000000..04df6ec4f8d89 --- /dev/null +++ b/pandas/tests/util/test_doc.py @@ -0,0 +1,55 @@ +from textwrap import dedent + +from pandas.util._decorators import doc + + +@doc(method="cummax", operation="maximum") +def cummax(whatever): + """ + This is the {method} method. + + It computes the cumulative {operation}. + """ + + +@doc(cummax, method="cummin", operation="minimum") +def cummin(whatever): + pass + + +@doc(cummin, method="cumsum", operation="sum") +def cumsum(whatever): + pass + + +def test_docstring_formatting(): + docstr = dedent( + """ + This is the cummax method. + + It computes the cumulative maximum. + """ + ) + assert cummax.__doc__ == docstr + + +def test_doc_template_from_func(): + docstr = dedent( + """ + This is the cummin method. + + It computes the cumulative minimum. + """ + ) + assert cummin.__doc__ == docstr + + +def test_inherit_doc_template(): + docstr = dedent( + """ + This is the cumsum method. + + It computes the cumulative sum. + """ + ) + assert cumsum.__doc__ == docstr From dd3f4511c42c11810ce1c7ff943028937ef19f2f Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Sun, 19 Jan 2020 10:54:24 -0500 Subject: [PATCH 06/14] update contributing_docstring to use doc decorator --- .../development/contributing_docstring.rst | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/doc/source/development/contributing_docstring.rst b/doc/source/development/contributing_docstring.rst index cb32f0e1ee475..172d01f9ac486 100644 --- a/doc/source/development/contributing_docstring.rst +++ b/doc/source/development/contributing_docstring.rst @@ -937,33 +937,31 @@ classes. This helps us keep docstrings consistent, while keeping things clear for the user reading. It comes at the cost of some complexity when writing. Each shared docstring will have a base template with variables, like -``%(klass)s``. The variables filled in later on using the ``Substitution`` -decorator. Finally, docstrings can be appended to with the ``Appender`` -decorator. +``{klass}``. The variables filled in later on using the ``doc`` decorator. +Finally, docstrings can also be appended to with the ``doc`` decorator. In this example, we'll create a parent docstring normally (this is like ``pandas.core.generic.NDFrame``. Then we'll have two children (like ``pandas.core.series.Series`` and ``pandas.core.frame.DataFrame``). We'll -substitute the children's class names in this docstring. +substitute the class names in this docstring. .. code-block:: python class Parent: + @doc(klass="Parent") def my_function(self): - """Apply my function to %(klass)s.""" + """Apply my function to {klass}.""" ... class ChildA(Parent): - @Substitution(klass="ChildA") - @Appender(Parent.my_function.__doc__) + @doc(Parent.my_function, klass="ChildA") def my_function(self): ... class ChildB(Parent): - @Substitution(klass="ChildB") - @Appender(Parent.my_function.__doc__) + @doc(Parent.my_function, klass="ChildB") def my_function(self): ... @@ -972,18 +970,16 @@ The resulting docstrings are .. code-block:: python >>> print(Parent.my_function.__doc__) - Apply my function to %(klass)s. + Apply my function to Parent. >>> print(ChildA.my_function.__doc__) Apply my function to ChildA. >>> print(ChildB.my_function.__doc__) Apply my function to ChildB. -Notice two things: +Notice: 1. We "append" the parent docstring to the children docstrings, which are initially empty. -2. Python decorators are applied inside out. So the order is Append then - Substitution, even though Substitution comes first in the file. Our files will often contain a module-level ``_shared_doc_kwargs`` with some common substitution values (things like ``klass``, ``axes``, etc). @@ -992,14 +988,13 @@ You can substitute and append in one shot with something like .. code-block:: python - @Appender(template % _shared_doc_kwargs) + @doc(template, **_shared_doc_kwargs) def my_function(self): ... where ``template`` may come from a module-level ``_shared_docs`` dictionary -mapping function names to docstrings. Wherever possible, we prefer using -``Appender`` and ``Substitution``, since the docstring-writing processes is -slightly closer to normal. +mapping function names to docstrings. Wherever possible, we prefer using +``doc``, since the docstring-writing processes is slightly closer to normal. See ``pandas.core.generic.NDFrame.fillna`` for an example template, and ``pandas.core.series.Series.fillna`` and ``pandas.core.generic.frame.fillna`` From c57c51f947aa07206bbb74fa58d31d147e092bae Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Sun, 19 Jan 2020 11:01:09 -0500 Subject: [PATCH 07/14] update to use doc decorator in fillna --- pandas/core/frame.py | 4 ++-- pandas/core/generic.py | 16 +++++++++++----- pandas/core/series.py | 5 ++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 6dd3a415297db..d94b73cada392 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -46,6 +46,7 @@ Appender, Substitution, deprecate_kwarg, + doc, rewrite_axis_style_signature, ) from pandas.util._validators import ( @@ -4130,8 +4131,7 @@ def rename( errors=errors, ) - @Substitution(**_shared_doc_kwargs) - @Appender(NDFrame.fillna.__doc__) + @doc(NDFrame.fillna, **_shared_doc_kwargs) def fillna( self, value=None, diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 0c413cd473bbc..8831b10252e74 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -44,7 +44,12 @@ from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError -from pandas.util._decorators import Appender, Substitution, rewrite_axis_style_signature +from pandas.util._decorators import ( + Appender, + Substitution, + doc, + rewrite_axis_style_signature, +) from pandas.util._validators import ( validate_bool_kwarg, validate_fillna_kwargs, @@ -5801,6 +5806,7 @@ def infer_objects(self: FrameOrSeries) -> FrameOrSeries: # ---------------------------------------------------------------------- # Filling NA's + @doc(**_shared_doc_kwargs) def fillna( self: FrameOrSeries, value=None, @@ -5821,11 +5827,11 @@ def fillna( each index (for a Series) or column (for a DataFrame). Values not in the dict/Series/DataFrame will not be filled. This value cannot be a list. - method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None + method : {{'backfill', 'bfill', 'pad', 'ffill', None}}, default None Method to use for filling holes in reindexed Series pad / ffill: propagate last valid observation forward to next valid backfill / bfill: use next valid observation to fill gap. - axis : %(axes_single_arg)s + axis : {axes_single_arg} Axis along which to fill missing values. inplace : bool, default False If True, fill in-place. Note: this will modify any @@ -5845,7 +5851,7 @@ def fillna( Returns ------- - %(klass)s or None + {klass} or None Object with missing values filled or None if ``inplace=True``. See Also @@ -5889,7 +5895,7 @@ def fillna( Replace all NaN elements in column 'A', 'B', 'C', and 'D', with 0, 1, 2, and 3 respectively. - >>> values = {'A': 0, 'B': 1, 'C': 2, 'D': 3} + >>> values = {{'A': 0, 'B': 1, 'C': 2, 'D': 3}} >>> df.fillna(value=values) A B C D 0 0.0 2.0 2.0 0 diff --git a/pandas/core/series.py b/pandas/core/series.py index 33565bbedade6..8b74ec4f5366a 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -25,7 +25,7 @@ from pandas._libs import index as libindex, lib, reshape, tslibs from pandas._typing import Label from pandas.compat.numpy import function as nv -from pandas.util._decorators import Appender, Substitution +from pandas.util._decorators import Appender, Substitution, doc from pandas.util._validators import validate_bool_kwarg, validate_percentile from pandas.core.dtypes.common import ( @@ -4102,8 +4102,7 @@ def drop( errors=errors, ) - @Substitution(**_shared_doc_kwargs) - @Appender(generic.NDFrame.fillna.__doc__) + @doc(generic.NDFrame.fillna, **_shared_doc_kwargs) def fillna( self, value=None, From 6ba6f9fe7dfee26909152574c9098653c48b373e Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Sun, 19 Jan 2020 11:41:47 -0500 Subject: [PATCH 08/14] remove trailing whitespaces --- doc/source/development/contributing_docstring.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/development/contributing_docstring.rst b/doc/source/development/contributing_docstring.rst index 172d01f9ac486..c47d934dd82ac 100644 --- a/doc/source/development/contributing_docstring.rst +++ b/doc/source/development/contributing_docstring.rst @@ -993,7 +993,7 @@ You can substitute and append in one shot with something like ... where ``template`` may come from a module-level ``_shared_docs`` dictionary -mapping function names to docstrings. Wherever possible, we prefer using +mapping function names to docstrings. Wherever possible, we prefer using ``doc``, since the docstring-writing processes is slightly closer to normal. See ``pandas.core.generic.NDFrame.fillna`` for an example template, and From 1cc08152774f101786238c3732623e9f8b0cb3cb Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Mon, 20 Jan 2020 11:07:58 -0500 Subject: [PATCH 09/14] fix mypy import issue --- pandas/core/series.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index 8b74ec4f5366a..9fc18a7749311 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -70,6 +70,7 @@ is_empty_data, sanitize_array, ) +from pandas.core.generic import NDFrame from pandas.core.indexers import maybe_convert_indices from pandas.core.indexes.accessors import CombinedDatetimelikeProperties from pandas.core.indexes.api import ( @@ -4102,7 +4103,7 @@ def drop( errors=errors, ) - @doc(generic.NDFrame.fillna, **_shared_doc_kwargs) + @doc(NDFrame.fillna, **_shared_doc_kwargs) def fillna( self, value=None, From 95668db0d364fcaf75f7a8e4fb3f2bafa8f3ff42 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Wed, 22 Jan 2020 13:01:02 -0500 Subject: [PATCH 10/14] TST: reorganize test cases for doc decorator --- pandas/tests/util/test_doc.py | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pandas/tests/util/test_doc.py b/pandas/tests/util/test_doc.py index 04df6ec4f8d89..f448a179aa20e 100644 --- a/pandas/tests/util/test_doc.py +++ b/pandas/tests/util/test_doc.py @@ -3,8 +3,8 @@ from pandas.util._decorators import doc -@doc(method="cummax", operation="maximum") -def cummax(whatever): +@doc(method="cumsum", operation="sum") +def cumsum(whatever): """ This is the {method} method. @@ -12,44 +12,44 @@ def cummax(whatever): """ -@doc(cummax, method="cummin", operation="minimum") -def cummin(whatever): +@doc(cumsum, method="cummax", operation="maximum") +def cummax(whatever): pass -@doc(cummin, method="cumsum", operation="sum") -def cumsum(whatever): +@doc(cummax, method="cummin", operation="minimum") +def cummin(whatever): pass def test_docstring_formatting(): docstr = dedent( """ - This is the cummax method. + This is the cumsum method. - It computes the cumulative maximum. - """ + It computes the cumulative sum. + """ ) - assert cummax.__doc__ == docstr + assert cumsum.__doc__ == docstr def test_doc_template_from_func(): docstr = dedent( """ - This is the cummin method. + This is the cummax method. - It computes the cumulative minimum. - """ + It computes the cumulative maximum. + """ ) - assert cummin.__doc__ == docstr + assert cummax.__doc__ == docstr def test_inherit_doc_template(): docstr = dedent( """ - This is the cumsum method. + This is the cummin method. - It computes the cumulative sum. - """ + It computes the cumulative minimum. + """ ) - assert cumsum.__doc__ == docstr + assert cummin.__doc__ == docstr From 81b088834f51c558d48c1f89667d79115d489462 Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Wed, 22 Jan 2020 13:15:15 -0500 Subject: [PATCH 11/14] TST: add test case for docstring appending --- pandas/tests/util/test_doc.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pandas/tests/util/test_doc.py b/pandas/tests/util/test_doc.py index f448a179aa20e..7e5e24456b9a7 100644 --- a/pandas/tests/util/test_doc.py +++ b/pandas/tests/util/test_doc.py @@ -12,6 +12,22 @@ def cumsum(whatever): """ +@doc( + cumsum, + """ + Examples + -------- + + >>> cumavg([1, 2, 3]) + 2 + """, + method="cumavg", + operation="average", +) +def cumavg(whatever): + pass + + @doc(cumsum, method="cummax", operation="maximum") def cummax(whatever): pass @@ -33,6 +49,23 @@ def test_docstring_formatting(): assert cumsum.__doc__ == docstr +def test_docstring_appending(): + docstr = dedent( + """ + This is the cumavg method. + + It computes the cumulative average. + + Examples + -------- + + >>> cumavg([1, 2, 3]) + 2 + """ + ) + assert cumavg.__doc__ == docstr + + def test_doc_template_from_func(): docstr = dedent( """ From 88c6bf2830647ea65cbcfd13943e65afb5f86dee Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Wed, 22 Jan 2020 13:36:01 -0500 Subject: [PATCH 12/14] DOC: add details for doc decorator --- pandas/util/_decorators.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 2e8888807a85f..8203d50f238d9 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -252,7 +252,17 @@ def doc(*args: Union[str, Callable], **kwargs: str) -> Callable: A decorator take docstring templates, concatenate them and perform string substitution on it. - This decorator should be robust even if func.__doc__ is None. + 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. + + Parameters + ---------- + *args : str or callable + The string / docstring / docstring template to be appended in order + after default docstring under function. + **kwags : str + The string which would be used to format docstring template. """ def decorator(func: Callable) -> Callable: From 2103d611a92a3d46a685a751f7a1ed54b43397db Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Fri, 31 Jan 2020 20:07:40 -0500 Subject: [PATCH 13/14] TYP: update type for decoratorto use TypeVar --- pandas/util/_decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 8203d50f238d9..6a65c5b1fe1a6 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -265,7 +265,7 @@ def doc(*args: Union[str, Callable], **kwargs: str) -> Callable: The string which would be used to format docstring template. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: @wraps(func) def wrapper(*args, **kwargs) -> Callable: return func(*args, **kwargs) @@ -282,7 +282,7 @@ def wrapper(*args, **kwargs) -> Callable: wrapper._docstr_template = "".join(dedent(t) for t in templates) # type: ignore wrapper.__doc__ = wrapper._docstr_template.format(**kwargs) # type: ignore - return wrapper + return cast(F, wrapper) return decorator From f259bca3bf103864db161b000e78e34ea886f84c Mon Sep 17 00:00:00 2001 From: HH-MWB Date: Fri, 7 Feb 2020 08:39:35 -0500 Subject: [PATCH 14/14] TYP: preserving type annotations for decorated --- pandas/util/_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 6a65c5b1fe1a6..f87f8597031db 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -247,7 +247,7 @@ def wrapper(*args, **kwargs) -> Callable[..., Any]: return decorate -def doc(*args: Union[str, Callable], **kwargs: str) -> Callable: +def doc(*args: Union[str, Callable], **kwargs: str) -> Callable[[F], F]: """ A decorator take docstring templates, concatenate them and perform string substitution on it.