From 937afa9620672d64c832b0d2a0f61e3a5fbcddd9 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 14:59:59 +0200 Subject: [PATCH 01/20] set version to 0.3.0.dev0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f422cc..8eeb322 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def readme(): setup(name='docrep', - version='0.2.8', + version='0.3.0.dev0', description='Python package for docstring repetition', long_description=readme(), classifiers=[ From 46f4126858b6b28e493e1ae5e362dc71e726b96b Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 20:11:41 +0200 Subject: [PATCH 02/20] unify methods this commit depreceates all methods ending with `f` (get_sectionsf for instance) or `_s` (keep_params_s for instance). Instead we use overloaded functions that can be used as a decorator or with strings as input --- docrep/__init__.py | 557 ++++++++++++++++++++----------------------- docrep/decorators.py | 117 +++++++++ docs/index.rst | 4 +- 3 files changed, 381 insertions(+), 297 deletions(-) create mode 100644 docrep/decorators.py diff --git a/docrep/__init__.py b/docrep/__init__.py index 8c465f2..11fc5f3 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -1,21 +1,31 @@ +"""The documentation repetition module""" import types import six import inspect import re from warnings import warn - -def dedents(s): - warn("The dedent function has been depreceated and will be removed soon. " - "Use inspect.cleandoc instead", DeprecationWarning, stacklevel=2) - return inspect.cleandoc(s) +from docrep.decorators import ( + updates_docstring, reads_docstring, deprecated_method, deprecated_function) -__version__ = '0.2.8' +__version__ = '0.3.0.dev0' __author__ = 'Philipp Sommer' +__all__ = [ + "safe_modulo", + "delete_params", + "delete_types", + "delete_kwargs", + "keep_params", + "keep_types", + "DocstringProcessor", + "dedents", +] + + substitution_pattern = re.compile( r"""(?s)(?.*?)\)# key enclosed in brackets""", re.VERBOSE) @@ -102,8 +112,145 @@ def safe_modulo(s, meta, checked='', print_warning=True, stacklevel=2): print_warning=print_warning, stacklevel=stacklevel) +def delete_params(s, *params): + """ + Delete the given parameters from a string. + + Same as :meth:`delete_params` but does not use the :attr:`params` + dictionary + + Parameters + ---------- + s: str + The string of the parameters section + params: list of str + The names of the parameters to delete + + Returns + ------- + str + The modified string `s` without the descriptions of `params` + """ + patt = '(?s)' + '|'.join( + r'(?<=\n)' + s + r'\s*:.+?\n(?=\S+|$)' for s in params) + return re.sub(patt, '', '\n' + s.strip() + '\n').strip() + + +def delete_types(s, *types): + """ + Delete the given types from a string. + + Same as :meth:`delete_types` but does not use the :attr:`params` + dictionary + + Parameters + ---------- + s: str + The string of the returns like section + types: list of str + The type identifiers to delete + + Returns + ------- + str + The modified string `s` without the descriptions of `types` + """ + patt = '(?s)' + '|'.join( + r'(?<=\n)' + s + r'\n.+?\n(?=\S+|$)' for s in types) + return re.sub(patt, '', '\n' + s.strip() + '\n',).strip() + + +def delete_kwargs(s, args=None, kwargs=None): + """ + Delete the ``*args`` or ``**kwargs`` part from the parameters section. + + Either `args` or `kwargs` must not be None. + + Parameters + ---------- + s: str + The string to delete the args and kwargs from + args: None or str + The string for the args to delete + kwargs: None or str + The string for the kwargs to delete + + Notes + ----- + The type name of `args` in `s` has to be like ````*```` (i.e. the + `args` argument preceeded by a ``'*'`` and enclosed by double ``'`'``). + Similarily, the type name of `kwargs` in `s` has to be like + ````**```` + """ + if not args and not kwargs: + return s + types = [] + if args is not None: + types.append(r'`?`?\*%s`?`?' % args) + if kwargs is not None: + types.append(r'`?`?\*\*%s`?`?' % kwargs) + return delete_types(s, *types) + + +def keep_params(s, *params): + """ + Keep the given parameters from a string. + + Same as :meth:`keep_params` but does not use the :attr:`params` + dictionary + + Parameters + ---------- + s: str + The string of the parameters like section + params: list of str + The parameter names to keep + + Returns + ------- + str + The modified string `s` with only the descriptions of `params` + """ + patt = '(?s)' + '|'.join( + r'(?<=\n)' + s + r'\s*:.+?\n(?=\S+|$)' for s in params) + return ''.join(re.findall(patt, '\n' + s.strip() + '\n')).rstrip() + + +def keep_types(s, *types): + """ + Keep the given types from a string. + + Same as :meth:`keep_types` but does not use the :attr:`params` + dictionary + + Parameters + ---------- + s: str + The string of the returns like section + types: list of str + The type identifiers to keep + + Returns + ------- + str + The modified string `s` with only the descriptions of `types` + """ + patt = '(?s)' + '|'.join( + r'(?<=\n)' + s + r'\n.+?\n(?=\S+|$)' for s in types) + return ''.join(re.findall(patt, '\n' + s.strip() + '\n')).rstrip() + + +# assign delete_params a new name for the deprecation of the corresponding +# DocstringProcessor method +_delete_params_s = delete_params +_delete_types_s = delete_types +_delete_kwargs_s = delete_kwargs +_keep_params_s = keep_params +_keep_types_s = keep_types + + class DocstringProcessor(object): - """Class that is intended to process docstrings + """Class that is intended to process docstrings. It is, but only to minor extends, inspired by the :class:`matplotlib.docstring.Substitution` class. @@ -125,12 +272,12 @@ class DocstringProcessor(object): >>> print(doc_test.__doc__) That's My doc string - Use the :meth:`get_sectionsf` method to extract Parameter sections (or + Use the :meth:`get_sections` method to extract Parameter sections (or others) form the docstring for later usage (and make sure, that the docstring is dedented):: - >>> @d.get_sectionsf('docstring_example', - ... sections=['Parameters', 'Examples']) + >>> @d.get_sections(base='docstring_example', + ... sections=['Parameters', 'Examples']) ... @d.dedent ... def doc_test(a=1, b=2): ... ''' @@ -180,7 +327,7 @@ class DocstringProcessor(object): Another example uses non-dedented docstrings:: - >>> @d.get_sectionsf('not_dedented') + >>> @d.get_sections(base='not_dedented') ... def doc_test2(a=1): ... '''That's the summary ... @@ -243,7 +390,7 @@ def __init__(self, *args, **kwargs): ``*args`` and ``**kwargs`` Parameters that shall be used for the substitution. Note that you can only provide either ``*args`` or ``**kwargs``, furthermore most of the - methods like `get_sectionsf` require ``**kwargs`` to be provided.""" + methods like `get_sections` require ``**kwargs`` to be provided.""" if len(args) and len(kwargs): raise ValueError("Only positional or keyword args are allowed") self.params = args or kwargs @@ -283,7 +430,8 @@ def __call__(self, func): stacklevel=3) return self._set_object_doc(func, doc) - def get_sections(self, s, base, + @reads_docstring + def get_sections(self, s, base=None, sections=['Parameters', 'Other Parameters']): """ Method that extracts the specified sections out of the given string if @@ -304,8 +452,8 @@ def get_sections(self, s, base, Returns ------- - str - The replaced string + dict + A mapping from section identifier to section string References ---------- @@ -321,9 +469,14 @@ def get_sections(self, s, base, params = self.params # Remove the summary and dedent the rest s = self._remove_summary(s) + + ret = {} + for section in sections: - key = '%s.%s' % (base, section.lower().replace(' ', '_')) - params[key] = self._get_section(s, section) + ret[section] = section_doc = self._get_section(s, section) + if base: + key = '%s.%s' % (base, section.lower().replace(' ', '_')) + params[key] = section_doc return s def _remove_summary(self, s): @@ -344,30 +497,9 @@ def _get_section(self, s, section): except AttributeError: return '' - def get_sectionsf(self, *args, **kwargs): - """ - Decorator method to extract sections from a function docstring - - Parameters - ---------- - ``*args`` and ``**kwargs`` - See the :meth:`get_sections` method. Note, that the first argument - will be the docstring of the specified function - - Returns - ------- - function - Wrapper that takes a function as input and registers its sections - via the :meth:`get_sections` method""" - def func(f): - doc = f.__doc__ - self.get_sections(doc or '', *args, **kwargs) - return f - return func - def _set_object_doc(self, obj, doc, stacklevel=3): - """Convenience method to set the __doc__ attribute of a python object - """ + warn("The DocstringProcessor._set_object_doc method has been " + "depreceated.", DeprecationWarning) if isinstance(obj, types.MethodType) and six.PY2: obj = obj.im_func try: @@ -382,19 +514,8 @@ def _set_object_doc(self, obj, doc, stacklevel=3): raise return obj - def dedent(self, func): - """ - Dedent the docstring of a function and substitute with :attr:`params` - - Parameters - ---------- - func: function - function with the documentation to dedent and whose sections - shall be inserted from the :attr:`params` attribute""" - doc = func.__doc__ and self.dedents(func.__doc__, stacklevel=4) - return self._set_object_doc(func, doc) - - def dedents(self, s, stacklevel=3): + @updates_docstring + def dedent(self, s, stacklevel=3): """ Dedent a string and substitute with the :attr:`params` attribute @@ -409,31 +530,8 @@ def dedents(self, s, stacklevel=3): s = inspect.cleandoc(s) return safe_modulo(s, self.params, stacklevel=stacklevel) - def with_indent(self, indent=0): - """ - Substitute in the docstring of a function with indented :attr:`params` - - Parameters - ---------- - indent: int - The number of spaces that the substitution should be indented - - Returns - ------- - function - Wrapper that takes a function as input and substitutes it's - ``__doc__`` with the indented versions of :attr:`params` - - See Also - -------- - with_indents, dedent""" - def replace(func): - doc = func.__doc__ and self.with_indents( - func.__doc__, indent=indent, stacklevel=4) - return self._set_object_doc(func, doc) - return replace - - def with_indents(self, s, indent=0, stacklevel=3): + @updates_docstring + def with_indent(self, s, indent=0, stacklevel=3): """ Substitute a string with the indented :attr:`params` @@ -488,32 +586,8 @@ def delete_params(self, base_key, *params): -------- delete_types, keep_params""" self.params[ - base_key + '.no_' + '|'.join(params)] = self.delete_params_s( - self.params[base_key], params) - - @staticmethod - def delete_params_s(s, params): - """ - Delete the given parameters from a string - - Same as :meth:`delete_params` but does not use the :attr:`params` - dictionary - - Parameters - ---------- - s: str - The string of the parameters section - params: list of str - The names of the parameters to delete - - Returns - ------- - str - The modified string `s` without the descriptions of `params` - """ - patt = '(?s)' + '|'.join( - r'(?<=\n)' + s + r'\s*:.+?\n(?=\S+|$)' for s in params) - return re.sub(patt, '', '\n' + s.strip() + '\n').strip() + base_key + '.no_' + '|'.join(params)] = delete_params( + self.params[base_key], *params) def delete_kwargs(self, base_key, args=None, kwargs=None): """ @@ -549,39 +623,9 @@ def delete_kwargs(self, base_key, args=None, kwargs=None): base_key)) return ext = '.no' + ('_args' if args else '') + ('_kwargs' if kwargs else '') - self.params[base_key + ext] = self.delete_kwargs_s( - self.params[base_key], args, kwargs) - - @classmethod - def delete_kwargs_s(cls, s, args=None, kwargs=None): - """ - Deletes the ``*args`` or ``**kwargs`` part from the parameters section - - Either `args` or `kwargs` must not be None. - - Parameters - ---------- - s: str - The string to delete the args and kwargs from - args: None or str - The string for the args to delete - kwargs: None or str - The string for the kwargs to delete - - Notes - ----- - The type name of `args` in `s` has to be like ````*```` (i.e. the - `args` argument preceeded by a ``'*'`` and enclosed by double ``'`'``). - Similarily, the type name of `kwargs` in `s` has to be like - ````**````""" - if not args and not kwargs: - return s - types = [] - if args is not None: - types.append(r'`?`?\*%s`?`?' % args) - if kwargs is not None: - types.append(r'`?`?\*\*%s`?`?' % kwargs) - return cls.delete_types_s(s, types) + ret = delete_kwargs(self.params[base_key], args, kwargs) + self.params[base_key + ext] = ret + return ret def delete_types(self, base_key, out_key, *types): """ @@ -607,33 +651,10 @@ def delete_types(self, base_key, out_key, *types): See Also -------- - delete_params""" - self.params['%s.%s' % (base_key, out_key)] = self.delete_types_s( - self.params[base_key], types) - - @staticmethod - def delete_types_s(s, types): - """ - Delete the given types from a string - - Same as :meth:`delete_types` but does not use the :attr:`params` - dictionary - - Parameters - ---------- - s: str - The string of the returns like section - types: list of str - The type identifiers to delete - - Returns - ------- - str - The modified string `s` without the descriptions of `types` + delete_params """ - patt = '(?s)' + '|'.join( - r'(?<=\n)' + s + r'\n.+?\n(?=\S+|$)' for s in types) - return re.sub(patt, '', '\n' + s.strip() + '\n',).strip() + self.params['%s.%s' % (base_key, out_key)] = delete_types( + self.params[base_key], *types) def keep_params(self, base_key, *params): """ @@ -667,7 +688,7 @@ def keep_params(self, base_key, *params): >>> from docrep import DocstringProcessor >>> d = DocstringProcessor() - >>> @d.get_sectionsf('do_something') + >>> @d.get_sections(base='do_something') ... def do_something(a=1, b=2, c=3): ... ''' ... That's %(doc_key)s @@ -720,36 +741,12 @@ def keep_params(self, base_key, *params): ... pass """ - self.params[base_key + '.' + '|'.join(params)] = self.keep_params_s( - self.params[base_key], params) - - @staticmethod - def keep_params_s(s, params): - """ - Keep the given parameters from a string - - Same as :meth:`keep_params` but does not use the :attr:`params` - dictionary - - Parameters - ---------- - s: str - The string of the parameters like section - params: list of str - The parameter names to keep - - Returns - ------- - str - The modified string `s` with only the descriptions of `params` - """ - patt = '(?s)' + '|'.join( - r'(?<=\n)' + s + r'\s*:.+?\n(?=\S+|$)' for s in params) - return ''.join(re.findall(patt, '\n' + s.strip() + '\n')).rstrip() + self.params[base_key + '.' + '|'.join(params)] = keep_params( + self.params[base_key], *params) def keep_types(self, base_key, out_key, *types): """ - Method to keep only specific parameters from a parameter documentation. + Keep only specific parameters from a parameter documentation. This method extracts the given `type` from the `base_key` item in the :attr:`params` dictionary and creates a new item with the original @@ -778,7 +775,7 @@ def keep_types(self, base_key, out_key, *types): >>> from docrep import DocstringProcessor >>> d = DocstringProcessor() - >>> @d.get_sectionsf('do_something', sections=['Returns']) + >>> @d.get_sections(base='do_something', sections=['Returns']) ... def do_something(): ... ''' ... That's %(doc_key)s @@ -826,47 +823,24 @@ def keep_types(self, base_key, out_key, *types): ... %(do_something.returns.no_float)s''' ... return do_something()[1] """ - self.params['%s.%s' % (base_key, out_key)] = self.keep_types_s( - self.params[base_key], types) - - @staticmethod - def keep_types_s(s, types): - """ - Keep the given types from a string + self.params['%s.%s' % (base_key, out_key)] = keep_types( + self.params[base_key], *types) - Same as :meth:`keep_types` but does not use the :attr:`params` - dictionary - - Parameters - ---------- - s: str - The string of the returns like section - types: list of str - The type identifiers to keep - - Returns - ------- - str - The modified string `s` with only the descriptions of `types` - """ - patt = '(?s)' + '|'.join( - r'(?<=\n)' + s + r'\n.+?\n(?=\S+|$)' for s in types) - return ''.join(re.findall(patt, '\n' + s.strip() + '\n')).rstrip() + @reads_docstring + def save_docstring(self, s, base=None): + """Save a docstring from a function. - def save_docstring(self, key): + Like the :meth:`get_sections` method this method serves as a + descriptor for functions but saves the entire docstring. """ - Descriptor method to save a docstring from a function - - Like the :meth:`get_sectionsf` method this method serves as a - descriptor for functions but saves the entire docstring""" - def func(f): - self.params[key] = f.__doc__ or '' - return f - return func + if base is not None: + self.params[base] = s + return s + @reads_docstring def get_summary(self, s, base=None): """ - Get the summary of the given docstring + Get the summary of the given docstring. This method extracts the summary from the given docstring `s` which is basicly the part until two newlines appear @@ -883,35 +857,16 @@ def get_summary(self, s, base=None): Returns ------- str - The extracted summary""" + The extracted summary + """ summary = summary_patt.search(s).group() if base is not None: self.params[base + '.summary'] = summary return summary - def get_summaryf(self, *args, **kwargs): - """ - Extract the summary from a function docstring - - Parameters - ---------- - ``*args`` and ``**kwargs`` - See the :meth:`get_summary` method. Note, that the first argument - will be the docstring of the specified function - - Returns - ------- - function - Wrapper that takes a function as input and registers its summary - via the :meth:`get_summary` method""" - def func(f): - doc = f.__doc__ - self.get_summary(doc or '', *args, **kwargs) - return f - return func - + @reads_docstring def get_extended_summary(self, s, base=None): - """Get the extended summary from a docstring + """Get the extended summary from a docstring. This here is the extended summary @@ -928,7 +883,8 @@ def get_extended_summary(self, s, base=None): Returns ------- str - The extracted extended summary""" + The extracted extended summary + """ # Remove the summary and dedent s = self._remove_summary(s) ret = '' @@ -940,31 +896,9 @@ def get_extended_summary(self, s, base=None): self.params[base + '.summary_ext'] = ret return ret - def get_extended_summaryf(self, *args, **kwargs): - """Extract the extended summary from a function docstring - - This function can be used as a decorator to extract the extended - summary of a function docstring (similar to :meth:`get_sectionsf`). - - Parameters - ---------- - ``*args`` and ``**kwargs`` - See the :meth:`get_extended_summary` method. Note, that the first - argument will be the docstring of the specified function - - Returns - ------- - function - Wrapper that takes a function as input and registers its summary - via the :meth:`get_extended_summary` method""" - def func(f): - doc = f.__doc__ - self.get_extended_summary(doc or '', *args, **kwargs) - return f - return func - + @reads_docstring def get_full_description(self, s, base=None): - """Get the full description from a docstring + """Get the full description from a docstring. This here and the line above is the full description (i.e. the combination of the :meth:`get_summary` and the @@ -983,7 +917,8 @@ def get_full_description(self, s, base=None): Returns ------- str - The extracted full description""" + The extracted full description + """ summary = self.get_summary(s) extended_summary = self.get_extended_summary(s) ret = (summary + '\n\n' + extended_summary).strip() @@ -991,26 +926,58 @@ def get_full_description(self, s, base=None): self.params[base + '.full_desc'] = ret return ret + # ------------------ DEPRECATED METHODS ----------------------------------- + + @deprecated_method('dedent') + def dedents(self, *args, **kwargs): + pass + + @deprecated_method('get_sections', replace=False) + def get_sectionsf(self, *args, **kwargs): + return self.get_sections(base=args[0], *args[1:], **kwargs) + + @deprecated_method('with_indent') + def with_indents(self, *args, **kwargs): + pass + + @staticmethod + @deprecated_function(_delete_params_s, True, 'docrep.delete_params') + def delete_params_s(*args, **kwargs): + pass + + @staticmethod + @deprecated_function(_delete_types_s, True, 'docrep.delete_types') + def delete_types_s(*args, **kwargs): + pass + + @classmethod + @deprecated_method(_delete_kwargs_s, True, 'docrep.delete_kwargs') + def delete_kwargs_s(cls, *args, **kwargs): + pass + + @staticmethod + @deprecated_function(_keep_params_s, True, 'docrep.keep_params') + def keep_params_s(*args, **kwargs): + pass + + @staticmethod + @deprecated_function(_keep_types_s, True, 'docrep.keep_types') + def keep_types_s(*args, **kwargs): + pass + + @deprecated_method('get_summary', replace=False) + def get_summaryf(self, *args, **kwargs): + return self.get_summary(base=args[0], *args[1:], **kwargs) + + @deprecated_method('get_full_description', replace=False) def get_full_descriptionf(self, *args, **kwargs): - """Extract the full description from a function docstring + return self.get_full_description(base=args[0], *args[1:], **kwargs) - This function can be used as a decorator to extract the full - descriptions of a function docstring (similar to - :meth:`get_sectionsf`). + @deprecated_method('get_extended_summary', replace=False) + def get_extended_summaryf(self, *args, **kwargs): + return self.get_extended_summary(base=args[0], *args[1:], **kwargs) - Parameters - ---------- - ``*args`` and ``**kwargs`` - See the :meth:`get_full_description` method. Note, that the first - argument will be the docstring of the specified function - Returns - ------- - function - Wrapper that takes a function as input and registers its summary - via the :meth:`get_full_description` method""" - def func(f): - doc = f.__doc__ - self.get_full_description(doc or '', *args, **kwargs) - return f - return func +@deprecated_function(inspect.cleandoc, replacement_name='inspect.cleandoc') +def dedents(s): + pass \ No newline at end of file diff --git a/docrep/decorators.py b/docrep/decorators.py new file mode 100644 index 0000000..39df43b --- /dev/null +++ b/docrep/decorators.py @@ -0,0 +1,117 @@ +"""Decorator functions for overloaded and deprecated methods""" +import six +from warnings import warn +from functools import wraps + + +def updates_docstring(func): + """Decorate a method that updates the docstring of a function.""" + + @wraps(func) + def update_docstring(self, *args, **kwargs): + if not len(args) or isinstance(args[0], six.string_types): + return func(self, *args, **kwargs) + elif len(args) and callable(args[0]): + doc = func(self, args[0].__doc__, *args[1:], **kwargs) + args[0].__doc__ = doc + return args[0] + else: + def decorator(f): + doc = func(self, f.__doc__, *args, **kwargs) + f.__doc__ = doc + return f + return decorator + + return update_docstring + + +def reads_docstring(func): + """Decorate a method that accepts a string or function.""" + + @wraps(func) + def use_docstring(self, s=None, base=None, *args, **kwargs): + # if only the base key is provided, use this method + if s: + if callable(s): + return func(self, s.__doc__, base, *args, **kwargs) + else: + return func(self, s, base, *args, **kwargs) + elif base: + + def decorator(f): + func(self, f.__doc__, base, *args, **kwargs) + + return decorator + else: + return func(self, s, base, *args, **kwargs) + + return use_docstring + + +def deprecated_method(replacement, replace=True, replacement_name=None): + """Mark a method as deprecated. + + Parameters + ---------- + replacement: str or callable + The name of the method that replaces this one here, or the function + itself + replace: bool + If True, then the `replacement` method is called instead of the + original one + replacement_name: str + The name of the replacement function to use in the warning message + """ + + def decorate(func): + + msg = "The %s method is deprecated, use the %s method instead" % ( + func.__name__, + replacement_name or getattr(replacement, '__name__', replacement)) + + @wraps(func) + def deprecated(self, *args, **kwargs): + warn(msg, DeprecationWarning, stacklevel=2) + if callable(replacement) and replace: + return replacement(*args, **kwargs) + elif replace: + return getattr(self, replacement)(*args, **kwargs) + else: + return func(self, *args, **kwargs) + + return deprecated + + return decorate + + +def deprecated_function(replacement, replace=True, replacement_name=None): + """Mark a method as deprecated. + + Parameters + ---------- + replacement: callable + A function to call instead, in case `replace` is True + replace: bool + Whether to replace the call of the function with a call of + `replacement` + replacement_name: str + The name of the `replacement` function. If this is None, + `replacement.__name__` is used + """ + + def decorate(func): + + msg = "The %s function is deprecated, use the %s method instead" % ( + func.__name__, replacement_name or replacement.__name__) + + @wraps(func) + def deprecated(*args, **kwargs): + warn(msg, DeprecationWarning, stacklevel=2) + if replace: + return replacement(*args, **kwargs) + else: + return func(*args, **kwargs) + + return deprecated + + return decorate \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 15339a7..a83fab6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,7 +130,7 @@ is intended for. Hence, The code above could be rewritten via In [5]: docstrings = docrep.DocstringProcessor() - In [6]: @docstrings.get_sectionsf('do_something') + In [6]: @docstrings.get_sections(base='do_something') ...: @docstrings.dedent ...: def do_something(a, b): ...: """ @@ -180,7 +180,7 @@ example .. ipython:: - In [9]: @docstrings.get_sectionsf('do_something') + In [9]: @docstrings.get_sections(base='do_something') ...: def second_example_source(a, b): ...: """Summary is on the first line ...: From 7e7020fdbea9153e074dad86df2f08cb618b7b59 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 20:12:12 +0200 Subject: [PATCH 03/20] rename methods in test and add new tests for deprecated functions --- tests/test_docrep.py | 187 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 167 insertions(+), 20 deletions(-) diff --git a/tests/test_docrep.py b/tests/test_docrep.py index f9f6ce3..5cf3d5b 100644 --- a/tests/test_docrep.py +++ b/tests/test_docrep.py @@ -14,6 +14,10 @@ def assertWarns(self, *args, **kwargs): "Do nothing here since it is not implemented in Python2" return warnings.catch_warnings(record=True) + def assertWarnsRegex(self, *args, **kwargs): + "Do nothing here since it is not implemented in Python2" + return warnings.catch_warnings(record=True) + else: _BaseTest = unittest.TestCase @@ -171,7 +175,7 @@ def setUp(self): def tearDown(self): del self.ds - def test_get_sectionsf(self, indented=False): + def test_get_sections(self, indented=False): """Test whether the parameter sections are extracted correctly""" if indented: def indent(s): @@ -200,9 +204,10 @@ def test(): see_also_header + '\n' + see_also)) base = 'test' - decorator = self.ds.get_sectionsf( - base, sections=['Examples', 'Parameters', 'Other Parameters', - 'Returns', 'Notes', 'See Also', 'References']) + decorator = self.ds.get_sections( + base=base, + sections=['Examples', 'Parameters', 'Other Parameters', + 'Returns', 'Notes', 'See Also', 'References']) decorator(test) ds = self.ds @@ -215,11 +220,11 @@ def test(): self.assertEqual(ds.params[base + '.see_also'], see_also) self.assertEqual(ds.params[base + '.references'], '') - def test_get_sectionsf_indented(self): - self.test_get_sectionsf(indented=True) + def test_get_sections_indented(self): + self.test_get_sections(indented=True) def test_dedent(self): - self.test_get_sectionsf() + self.test_get_sections() with self.assertWarns(SyntaxWarning): @self.ds.dedent @@ -246,7 +251,7 @@ def test2(): self.assertEqual(test2.__doc__, ref) def test_with_indent(self): - self.test_get_sectionsf_indented() + self.test_get_sections_indented() with self.assertWarns(SyntaxWarning): @self.ds.with_indent(16) @@ -276,7 +281,7 @@ def test2(): self.assertEqual(s, ref) def test_dedents(self): - self.test_get_sectionsf() + self.test_get_sections() s = """ A test function with used docstring from another @@ -303,7 +308,7 @@ def test_dedents(self): def test_save_docstring(self): """Test the :meth:`docrep.DocstringProcessor.save_docstring` method""" - @self.ds.save_docstring('test') + @self.ds.save_docstring(base='test') def test(): "Just a test\n\nwith something" pass @@ -311,7 +316,7 @@ def test(): self.assertEqual(self.ds.params['test'], "Just a test\n\nwith something") - def test_get_summaryf(self): + def test_get_summary(self): """Test whether the summary is extracted correctly""" doc = ( @@ -321,20 +326,20 @@ def test_oneline(): pass test_oneline.__doc__ = summary + '\n\n' + doc - self.ds.get_summaryf('test1')(test_oneline) + self.ds.get_summary(base='test1')(test_oneline) self.assertEqual(self.ds.params['test1.summary'], summary) def test_multiline(): pass test_multiline.__doc__ = multiline_summary + '\n\n' + doc - self.ds.get_summaryf('test2')(test_multiline) + self.ds.get_summary(base='test2')(test_multiline) self.assertEqual(self.ds.params['test2.summary'], multiline_summary) def test_summary_only(): pass test_summary_only.__doc__ = summary - self.ds.get_summaryf('test3')(test_summary_only) + self.ds.get_summary(base='test3')(test_summary_only) self.assertEqual(self.ds.params['test3.summary'], summary) def test_get_extended_summary(self): @@ -347,7 +352,7 @@ def test_basic(): pass test_basic.__doc__ = summary + '\n\n' + doc - self.ds.get_extended_summaryf('test1')(test_basic) + self.ds.get_extended_summary(base='test1')(test_basic) self.assertEqual(self.ds.params['test1.summary_ext'], random_text.strip()) @@ -355,13 +360,13 @@ def test_no_extended_summary(): pass test_no_extended_summary.__doc__ = doc - self.ds.get_extended_summaryf('test2')(test_no_extended_summary) + self.ds.get_extended_summary(base='test2')(test_no_extended_summary) self.assertEqual(self.ds.params['test2.summary_ext'], '') def test_no_params(): pass test_no_params.__doc__ = summary + '\n\n' + random_text - self.ds.get_extended_summaryf('test3')(test_no_params) + self.ds.get_extended_summary(base='test3')(test_no_params) self.assertEqual(self.ds.params['test3.summary_ext'], random_text.strip()) @@ -375,7 +380,7 @@ def test_basic(): pass test_basic.__doc__ = summary + '\n\n' + doc - self.ds.get_full_descriptionf('test1')(test_basic) + self.ds.get_full_description(base='test1')(test_basic) self.assertEqual(self.ds.params['test1.full_desc'], summary + '\n\n' + random_text.strip()) @@ -383,14 +388,14 @@ def test_no_extended_summary(): pass test_no_extended_summary.__doc__ = doc - self.ds.get_full_descriptionf('test2')(test_no_extended_summary) + self.ds.get_full_description(base='test2')(test_no_extended_summary) self.assertEqual(self.ds.params['test2.full_desc'], random_text.strip()) def test_no_params(): pass test_no_params.__doc__ = summary + '\n\n' + random_text - self.ds.get_full_descriptionf('test3')(test_no_params) + self.ds.get_full_description(base='test3')(test_no_params) self.assertEqual(self.ds.params['test3.full_desc'], summary + '\n\n' + random_text.strip()) @@ -611,5 +616,147 @@ class Test2(object): self.fail("Should have raised AttributeError!") +class DepreceationsTest(_BaseTest): + """Test case for depreceated methods""" + + def setUp(self): + self.ds = docrep.DocstringProcessor() + + def tearDown(self): + del self.ds + + def test_get_sectionsf(self, indented=False): + """Test whether the parameter sections are extracted correctly""" + if indented: + def indent(s): + return ' ' * 4 + ('\n' + ' ' * 4).join(s.splitlines()) + else: + def indent(s): + return s + + self.params_section = ps = simple_param + '\n' + complex_param + self.other_params_section = ops = ( + simple_multiline_param + '\n' + very_complex_param) + self.returns_section = rs = ( + simple_return_type + '\n' + very_complex_return_type) + + def test(): + pass + + test.__doc__ = ( + summary + '\n\n' + indent( + random_text + '\n\n' + + parameters_header + '\n' + ps + '\n\n' + + other_parameters_header + '\n' + ops + '\n\n' + + returns_header + '\n' + rs + '\n\n' + + examples_header + '\n' + examples + '\n\n' + + notes_header + '\n' + notes + '\n\n' + + see_also_header + '\n' + see_also)) + base = 'test' + + with self.assertWarnsRegex(DeprecationWarning, 'get_sectionsf'): + decorator = self.ds.get_sectionsf( + base, sections=['Examples', 'Parameters', 'Other Parameters', + 'Returns', 'Notes', 'See Also', 'References']) + decorator(test) + + ds = self.ds + self.assertEqual(ds.params[base + '.parameters'], ps) + self.assertEqual(ds.params[base + '.other_parameters'], + ops) + self.assertEqual(ds.params[base + '.returns'], rs) + self.assertEqual(ds.params[base + '.examples'], examples) + self.assertEqual(ds.params[base + '.notes'], notes) + self.assertEqual(ds.params[base + '.see_also'], see_also) + self.assertEqual(ds.params[base + '.references'], '') + + def test_dedents(self): + self.test_get_sectionsf() + s = """ + A test function with used docstring from another + + Parameters + ---------- + %(test.parameters)s + %(missing)s + + Examples + -------- + %(test.examples)s""" + + ref = ("A test function with used docstring from another\n" + "\n" + "Parameters\n" + "----------\n" + + self.params_section + '\n%(missing)s\n\n' + + examples_header + '\n' + examples) + + with self.assertWarnsRegex(DeprecationWarning, 'dedents'): + res = self.ds.dedents(s) + + self.assertEqual(res, ref) + + def test_keep_params_s(self): + all_pdescs = [simple_param, simple_param2, complex_param, + very_complex_param, simple_multiline_param] + pdescs = [simple_param, very_complex_param] + params = [pdesc.splitlines()[0].split(':')[0].strip() + for pdesc in pdescs] + joined_pdescs = '\n'.join(pdescs) + ds = docrep.DocstringProcessor() + with self.assertWarnsRegex(DeprecationWarning, 'keep_params_s'): + txt = ds.keep_params_s('\n'.join(all_pdescs), *params) + # check single + self.assertEqual(txt, joined_pdescs, + msg='Wrong description for params {}'.format(params)) + + def test_get_summaryf(self): + """Test whether the summary is extracted correctly""" + + doc = ( + random_text + '\n\n' + parameters_header + '\n' + complex_param) + + def test_oneline(): + pass + test_oneline.__doc__ = summary + '\n\n' + doc + + with self.assertWarnsRegex(DeprecationWarning, 'get_summaryf'): + self.ds.get_summaryf('test1')(test_oneline) + self.assertEqual(self.ds.params['test1.summary'], summary) + + + def test_get_extended_summary(self): + """Test whether the extended summary is extracted correctly""" + + doc = ( + random_text + '\n\n' + parameters_header + '\n' + complex_param) + + def test_basic(): + pass + test_basic.__doc__ = summary + '\n\n' + doc + + with self.assertWarnsRegex(DeprecationWarning, + 'get_extended_summaryf'): + self.ds.get_extended_summaryf('test1')(test_basic) + self.assertEqual(self.ds.params['test1.summary_ext'], + random_text.strip()) + + def test_get_full_description(self): + """Test whether the full description is extracted correctly""" + + doc = ( + random_text + '\n\n' + parameters_header + '\n' + complex_param) + + def test_basic(): + pass + test_basic.__doc__ = summary + '\n\n' + doc + + with self.assertWarnsRegex(DeprecationWarning, + 'get_full_descriptionf'): + self.ds.get_full_descriptionf('test1')(test_basic) + self.assertEqual(self.ds.params['test1.full_desc'], + summary + '\n\n' + random_text.strip()) + + if __name__ == '__main__': unittest.main() From a6fb02a397d39a8d0fc56530cc27c7acd5f1f725 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:28:18 +0200 Subject: [PATCH 04/20] use _set_object_doc for updating docstrings to ensure python 2.7 compatibility (for the last time...) --- docrep/__init__.py | 35 +++++++++-------------------------- docrep/decorators.py | 26 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docrep/__init__.py b/docrep/__init__.py index 11fc5f3..e11c5a0 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -22,7 +22,6 @@ "keep_params", "keep_types", "DocstringProcessor", - "dedents", ] @@ -412,7 +411,8 @@ def __init__(self, *args, **kwargs): self._all_sections_patt = re.compile(all_sections_patt) self.patterns = patterns - def __call__(self, func): + @updates_docstring + def __call__(self, s): """ Substitute in a docstring of a function with :attr:`params` @@ -426,9 +426,7 @@ def __call__(self, func): -------- dedent: also dedents the doc with_indent: also indents the doc""" - doc = func.__doc__ and safe_modulo(func.__doc__, self.params, - stacklevel=3) - return self._set_object_doc(func, doc) + return safe_modulo(s, self.params, stacklevel=3) @reads_docstring def get_sections(self, s, base=None, @@ -497,27 +495,10 @@ def _get_section(self, s, section): except AttributeError: return '' - def _set_object_doc(self, obj, doc, stacklevel=3): - warn("The DocstringProcessor._set_object_doc method has been " - "depreceated.", DeprecationWarning) - if isinstance(obj, types.MethodType) and six.PY2: - obj = obj.im_func - try: - obj.__doc__ = doc - except AttributeError: # probably python2 class - if (self.python2_classes != 'raise' and - (inspect.isclass(obj) and six.PY2)): - if self.python2_classes == 'warn': - warn("Cannot modify docstring of classes in python2!", - stacklevel=stacklevel) - else: - raise - return obj - @updates_docstring def dedent(self, s, stacklevel=3): """ - Dedent a string and substitute with the :attr:`params` attribute + Dedent a string and substitute with the :attr:`params` attribute. Parameters ---------- @@ -526,14 +507,15 @@ def dedent(self, s, stacklevel=3): attribute stacklevel: int The stacklevel for the warning raised in :func:`safe_module` when - encountering an invalid key in the string""" + encountering an invalid key in the string + """ s = inspect.cleandoc(s) return safe_modulo(s, self.params, stacklevel=stacklevel) @updates_docstring def with_indent(self, s, indent=0, stacklevel=3): """ - Substitute a string with the indented :attr:`params` + Substitute a string with the indented :attr:`params`. Parameters ---------- @@ -552,7 +534,8 @@ def with_indent(self, s, indent=0, stacklevel=3): See Also -------- - with_indent, dedents""" + with_indent, dedent + """ # we make a new dictionary with objects that indent the original # strings if necessary. Note that the first line is not indented d = {key: _StrWithIndentation(val, indent) diff --git a/docrep/decorators.py b/docrep/decorators.py index 39df43b..32d7b34 100644 --- a/docrep/decorators.py +++ b/docrep/decorators.py @@ -1,5 +1,7 @@ """Decorator functions for overloaded and deprecated methods""" import six +import types +import inspect from warnings import warn from functools import wraps @@ -13,12 +15,12 @@ def update_docstring(self, *args, **kwargs): return func(self, *args, **kwargs) elif len(args) and callable(args[0]): doc = func(self, args[0].__doc__, *args[1:], **kwargs) - args[0].__doc__ = doc + _set_object_doc(args[0], doc, py2_class=self.python2_classes) return args[0] else: def decorator(f): doc = func(self, f.__doc__, *args, **kwargs) - f.__doc__ = doc + _set_object_doc(f, doc, py2_class=self.python2_classes) return f return decorator @@ -114,4 +116,22 @@ def deprecated(*args, **kwargs): return deprecated - return decorate \ No newline at end of file + return decorate + + +def _set_object_doc(obj, doc, stacklevel=3, py2_class='warn'): + warn("The DocstringProcessor._set_object_doc method has been " + "depreceated.", DeprecationWarning) + if isinstance(obj, types.MethodType) and six.PY2: + obj = obj.im_func + try: + obj.__doc__ = doc + except AttributeError: # probably python2 class + if (py2_class!= 'raise' and + (inspect.isclass(obj) and six.PY2)): + if py2_class == 'warn': + warn("Cannot modify docstring of classes in python2!", + stacklevel=stacklevel) + else: + raise + return obj \ No newline at end of file From 72c5957e7bb76c44dad7b1bf309cf306f8dc2c9b Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:30:03 +0200 Subject: [PATCH 05/20] run tests for python 3.8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 137869e..58122d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" dist: xenial install: - pip install coveralls pytest From f74765730aee8fae17e7ecde596e24f02e119c28 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:49:54 +0200 Subject: [PATCH 06/20] add migration from 0.2.8 to 0.3 to changelog --- CHANGELOG.rst | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6f2b457..caadc28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,50 @@ +v0.3.0 +====== +New framework for decorators + +This release deprecates several methods of the :class:`DocstringProcessor` in +favor of a more uniform framework. Functions such as `get_sections` and `dedent` +now work for both, as decorators and directly on strings. See +:ref:`migrate-to-0.3` down below + +.. _changed-in-0.3: + +Changed +------- +- The following methods of the :class:`DocstringProcessor` class have been + deprecated: + + docstring update methods for strings: + * ``dedents`` in favor of :meth:`~DocstringProcessor.dedent` + * ``with_indents`` in favor of :meth:`~DocstringProcessor.with_indent` + docstring analysis decorators + * ``get_sectionsf`` in favor of :meth:`~DocstringProcessor.get_sections` + * ``get_summaryf`` in favor of :meth:`~DocstringProcessor.get_summary` + * ``get_full_descriptionf`` in favor of :meth:`~DocstringProcessor.get_full_description` + * ``get_extended_summaryf`` in favor of :meth:`~DocstringProcessor.get_extended_summary` + docstring parameter and type extractors for strings + * ``delete_params_s`` in favor of :func:`docrep.delete_params` + * ``delete_types_s`` in favor of :func:`docrep.delete_types` + * ``delete_kwargs_s`` in favor of :func:`docrep.delete_kwargs` + * ``keep_params_s`` in favor of :func:`docrep.keep_params` + * ``keep_types_s`` in favor of :func:`docrep.keep_types` + +.. _migrate-to-0.3: + +Migrating from 0.2.8 to 0.3.0 +----------------------------- +Migration is possible using the following steps: + +* For the deprecated update methods (see the :ref:`changes above `), + just use the above-mentioned replacement. They work for both, as decorators and + with strings. +* For the analysis decorators (``get_sectionsf`` for instance, use the replacement) + but you need to explicitly state the `base` parameter. + ``@get_sectionsf('something')`` for instance needs to be replaced with + ``@get_sections(base='something')`` +* for the parameter and type extractor functions, just use the corresponding + module level function mentioned :ref:`above ` + v0.2.8 ====== Minor patch to solve deprecation warnings for various regular expressions. From 4c80e516f630b589d517d84e5f7ea8ead2b18f29 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:54:21 +0200 Subject: [PATCH 07/20] config updates for docs --- docs/conf.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 08e4cac..7826acd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,16 +62,19 @@ # The master toctree document. master_doc = 'index' -autodoc_default_flags = ['show_inheritance', 'autosummary', - 'show-formatoptions'] +autodoc_default_options = { + 'show_inheritance': True, + 'autosummary': True, +} + autoclass_content = 'both' ipython_savefig_dir = os.path.join(os.path.dirname(__file__), '_static') # General information about the project. project = u'docrep' -copyright = u'2016, Philipp Sommer' -author = u'Philipp Sommer' +copyright = u'2016-2020, Philipp S. Sommer' +author = u'Philipp S. Sommer' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -253,7 +256,7 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'docrep.tex', u'docrep Documentation', - u'Philipp Sommer', 'manual'), + u'Philipp S. Sommer', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -385,15 +388,9 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None), - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'matplotlib': ('http://matplotlib.org/', None), - 'sphinx': ('http://www.sphinx-doc.org/en/stable/', None), - 'xarray': ('http://xarray.pydata.org/en/stable/', None), - 'cartopy': ('http://scitools.org.uk/cartopy/docs/latest/', None), - 'mpl_toolkits': ('http://matplotlib.org/basemap/', None), + 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), } if six.PY3: - intersphinx_mapping['python'] = ('https://docs.python.org/3.4/', None) + intersphinx_mapping['python'] = ('https://docs.python.org/3/', None) else: intersphinx_mapping['python'] = ('https://docs.python.org/2.7/', None) From 605a635f9ec0a39b18c52bd2c22b1ef40e0116e4 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:54:59 +0200 Subject: [PATCH 08/20] update contact details --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8eeb322..935a76d 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,8 @@ def readme(): ], keywords='docstrings docs docstring napoleon numpy reStructured text', url='https://github.com/Chilipp/docrep', - author='Philipp Sommer', - author_email='philipp.sommer@unil.ch', + author='Philipp S. Sommer', + author_email='philipp.sommer@hzg.de', license="GPLv2", packages=find_packages(exclude=['docs', 'tests*', 'examples']), install_requires=[ From abadbba3dc96071ccc8e711071f618ed1729d932 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 21:57:38 +0200 Subject: [PATCH 09/20] add project_urls --- setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.py b/setup.py index 935a76d..d8d590b 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def readme(): 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: OS Independent', ], keywords='docstrings docs docstring napoleon numpy reStructured text', @@ -40,4 +41,9 @@ def readme(): data_files=[("", ["LICENSE"])], setup_requires=pytest_runner, tests_require=['pytest'], + project_urls={ + 'Documentation': 'https://docrep.readthedocs.io', + 'Source': 'https://github.com/Chilipp/docrep', + 'Tracker': 'https://github.com/Chilipp/docrep/issues', + }, zip_safe=False) From 4436886ce4fa4d516d22b770ae62c75654960764 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 22:10:14 +0200 Subject: [PATCH 10/20] deprecate save_docstring in favor of get_docstring --- CHANGELOG.rst | 1 + docrep/__init__.py | 8 ++++++-- tests/test_docrep.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index caadc28..34c386b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,7 @@ Changed * ``get_summaryf`` in favor of :meth:`~DocstringProcessor.get_summary` * ``get_full_descriptionf`` in favor of :meth:`~DocstringProcessor.get_full_description` * ``get_extended_summaryf`` in favor of :meth:`~DocstringProcessor.get_extended_summary` + * ``save_docstring`` in favor of :meth:`~DocstringProcessorget_docstring` docstring parameter and type extractors for strings * ``delete_params_s`` in favor of :func:`docrep.delete_params` * ``delete_types_s`` in favor of :func:`docrep.delete_types` diff --git a/docrep/__init__.py b/docrep/__init__.py index e11c5a0..5865f19 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -810,8 +810,8 @@ def keep_types(self, base_key, out_key, *types): self.params[base_key], *types) @reads_docstring - def save_docstring(self, s, base=None): - """Save a docstring from a function. + def get_docstring(self, s, base=None): + """get a docstring from a function. Like the :meth:`get_sections` method this method serves as a descriptor for functions but saves the entire docstring. @@ -948,6 +948,10 @@ def keep_params_s(*args, **kwargs): def keep_types_s(*args, **kwargs): pass + @deprecated_method('get_docstring', replace=False) + def save_docstring(self, *args, **kwargs): + return self.get_docstring(base=args[0], *args[1:], **kwargs) + @deprecated_method('get_summary', replace=False) def get_summaryf(self, *args, **kwargs): return self.get_summary(base=args[0], *args[1:], **kwargs) diff --git a/tests/test_docrep.py b/tests/test_docrep.py index 5cf3d5b..9b9e3e4 100644 --- a/tests/test_docrep.py +++ b/tests/test_docrep.py @@ -306,9 +306,9 @@ def test_dedents(self): self.assertEqual(res, ref) - def test_save_docstring(self): + def test_get_docstring(self): """Test the :meth:`docrep.DocstringProcessor.save_docstring` method""" - @self.ds.save_docstring(base='test') + @self.ds.get_docstring(base='test') def test(): "Just a test\n\nwith something" pass @@ -757,6 +757,20 @@ def test_basic(): self.assertEqual(self.ds.params['test1.full_desc'], summary + '\n\n' + random_text.strip()) + def test_save_docstring(self): + """Test the :meth:`docrep.DocstringProcessor.save_docstring` method""" + def test(): + "Just a test\n\nwith something" + pass + + with self.assertWarnsRegex(DeprecationWarning, 'save_docstring'): + self.ds.save_docstring('test')(test) + + self.assertEqual(self.ds.params['test'], + "Just a test\n\nwith something") + + + if __name__ == '__main__': unittest.main() From 9af36e7dfb6f9212d56cac5a06e41b454bda093d Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 23:05:11 +0200 Subject: [PATCH 11/20] add warning about deprecated methods --- docs/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index a83fab6..341e349 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,11 @@ A Python Module for intelligent reuse of docstrings index +.. warning:: + + Several methods have been deprecated in version 0.3. See :ref:`migrate-to-0.3` + for details! + .. only:: html and not epub .. list-table:: From 46477f40bb0f074d7b9600459bffcb33112158f6 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 23:05:36 +0200 Subject: [PATCH 12/20] modify docstring of deprecated methods --- docrep/__init__.py | 27 ++++++++++++------------ docrep/decorators.py | 49 +++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/docrep/__init__.py b/docrep/__init__.py index 5865f19..16f810d 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -911,60 +911,61 @@ def get_full_description(self, s, base=None): # ------------------ DEPRECATED METHODS ----------------------------------- - @deprecated_method('dedent') + @deprecated_method('dedent', "0.3.0") def dedents(self, *args, **kwargs): pass - @deprecated_method('get_sections', replace=False) + @deprecated_method('get_sections', "0.3.0", replace=False) def get_sectionsf(self, *args, **kwargs): return self.get_sections(base=args[0], *args[1:], **kwargs) - @deprecated_method('with_indent') + @deprecated_method('with_indent', "0.3.0") def with_indents(self, *args, **kwargs): pass @staticmethod - @deprecated_function(_delete_params_s, True, 'docrep.delete_params') + @deprecated_function(_delete_params_s, "0.3.0", True, 'docrep.delete_params') def delete_params_s(*args, **kwargs): pass @staticmethod - @deprecated_function(_delete_types_s, True, 'docrep.delete_types') + @deprecated_function(_delete_types_s, "0.3.0", True, 'docrep.delete_types') def delete_types_s(*args, **kwargs): pass @classmethod - @deprecated_method(_delete_kwargs_s, True, 'docrep.delete_kwargs') + @deprecated_method(_delete_kwargs_s, "0.3.0", True, 'docrep.delete_kwargs') def delete_kwargs_s(cls, *args, **kwargs): pass @staticmethod - @deprecated_function(_keep_params_s, True, 'docrep.keep_params') + @deprecated_function(_keep_params_s, "0.3.0", True, 'docrep.keep_params') def keep_params_s(*args, **kwargs): pass @staticmethod - @deprecated_function(_keep_types_s, True, 'docrep.keep_types') + @deprecated_function(_keep_types_s, "0.3.0", True, 'docrep.keep_types') def keep_types_s(*args, **kwargs): pass - @deprecated_method('get_docstring', replace=False) + @deprecated_method('get_docstring', "0.3.0", replace=False) def save_docstring(self, *args, **kwargs): return self.get_docstring(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_summary', replace=False) + @deprecated_method('get_summary', "0.3.0", replace=False) def get_summaryf(self, *args, **kwargs): return self.get_summary(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_full_description', replace=False) + @deprecated_method('get_full_description', "0.3.0", replace=False) def get_full_descriptionf(self, *args, **kwargs): return self.get_full_description(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_extended_summary', replace=False) + @deprecated_method('get_extended_summary', "0.3.0", replace=False) def get_extended_summaryf(self, *args, **kwargs): return self.get_extended_summary(base=args[0], *args[1:], **kwargs) -@deprecated_function(inspect.cleandoc, replacement_name='inspect.cleandoc') +@deprecated_function(inspect.cleandoc, "0.2.6", + replacement_name='inspect.cleandoc') def dedents(s): pass \ No newline at end of file diff --git a/docrep/decorators.py b/docrep/decorators.py index 32d7b34..26d9b3a 100644 --- a/docrep/decorators.py +++ b/docrep/decorators.py @@ -3,13 +3,22 @@ import types import inspect from warnings import warn -from functools import wraps +import functools + + +deprecated_doc = """ + Deprecated {type} + + .. deprecated:: {version} + + Use :{type_short}:`{replacement}` instead! +""" def updates_docstring(func): """Decorate a method that updates the docstring of a function.""" - @wraps(func) + @functools.wraps(func) def update_docstring(self, *args, **kwargs): if not len(args) or isinstance(args[0], six.string_types): return func(self, *args, **kwargs) @@ -30,7 +39,7 @@ def decorator(f): def reads_docstring(func): """Decorate a method that accepts a string or function.""" - @wraps(func) + @functools.wraps(func) def use_docstring(self, s=None, base=None, *args, **kwargs): # if only the base key is provided, use this method if s: @@ -50,7 +59,8 @@ def decorator(f): return use_docstring -def deprecated_method(replacement, replace=True, replacement_name=None): +def deprecated_method(replacement, version, replace=True, + replacement_name=None): """Mark a method as deprecated. Parameters @@ -65,13 +75,14 @@ def deprecated_method(replacement, replace=True, replacement_name=None): The name of the replacement function to use in the warning message """ + replacement_name = replacement_name or getattr( + replacement, '__name__', replacement) + def decorate(func): msg = "The %s method is deprecated, use the %s method instead" % ( - func.__name__, - replacement_name or getattr(replacement, '__name__', replacement)) + func.__name__, replacement_name) - @wraps(func) def deprecated(self, *args, **kwargs): warn(msg, DeprecationWarning, stacklevel=2) if callable(replacement) and replace: @@ -81,12 +92,19 @@ def deprecated(self, *args, **kwargs): else: return func(self, *args, **kwargs) - return deprecated + deprecated.__doc__ = deprecated_doc.format( + type="method", version=version, replacement=replacement_name, + type_short="meth") + + return functools.wraps( + func, assigned=set(functools.WRAPPER_ASSIGNMENTS) - {'__doc__'})( + deprecated) return decorate -def deprecated_function(replacement, replace=True, replacement_name=None): +def deprecated_function(replacement, version, replace=True, + replacement_name=None): """Mark a method as deprecated. Parameters @@ -101,12 +119,13 @@ def deprecated_function(replacement, replace=True, replacement_name=None): `replacement.__name__` is used """ + replacement_name = replacement_name or replacement.__name__ + def decorate(func): msg = "The %s function is deprecated, use the %s method instead" % ( - func.__name__, replacement_name or replacement.__name__) + func.__name__, replacement_name) - @wraps(func) def deprecated(*args, **kwargs): warn(msg, DeprecationWarning, stacklevel=2) if replace: @@ -114,7 +133,13 @@ def deprecated(*args, **kwargs): else: return func(*args, **kwargs) - return deprecated + deprecated.__doc__ = deprecated_doc.format( + type="function", version=version, replacement=replacement_name, + type_short="func") + + return functools.wraps( + func, assigned=set(functools.WRAPPER_ASSIGNMENTS) - {'__doc__'})( + deprecated) return decorate From aa0ad1b27aca0217ffcf6a8a6f92787b3b2b6485 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 23:06:06 +0200 Subject: [PATCH 13/20] group DocstringProcessor methods using the autosummary by autodocsumm --- docs/conf.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 7826acd..8eeb62e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -394,3 +394,25 @@ intersphinx_mapping['python'] = ('https://docs.python.org/3/', None) else: intersphinx_mapping['python'] = ('https://docs.python.org/2.7/', None) + + +sections = { + 'Updating Methods': ['dedent', 'with_indent'], + 'Analysis Methods': ['get_sections', 'get_summary', 'get_extended_summary', + 'get_full_description', 'get_docstring'], + 'Extraction Methods': ['delete_params', 'delete_kwargs', 'delete_types', + 'keep_params', 'keep_types'] + } + + +def group_docstring_processor_methods(app, what, name, obj, section, parent): + if parent is docrep.DocstringProcessor: + for section, vals in sections.items(): + if name in vals: + return section + if obj.__doc__.strip().startswith("Deprecated"): + return "Deprecated Methods" + + +def setup(app): + app.connect('autodocsumm-grouper', group_docstring_processor_methods) From 73014d8b9bd2a68ff4ce235166833bd3b5969ead Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Fri, 10 Jul 2020 23:15:22 +0200 Subject: [PATCH 14/20] minor documentation fixes --- docrep/__init__.py | 49 ++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/docrep/__init__.py b/docrep/__init__.py index 16f810d..8617b6f 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -1,5 +1,4 @@ """The documentation repetition module""" -import types import six import inspect import re @@ -384,12 +383,16 @@ class DocstringProcessor(object): def __init__(self, *args, **kwargs): """ - Parameters - ---------- - ``*args`` and ``**kwargs`` - Parameters that shall be used for the substitution. Note that you can - only provide either ``*args`` or ``**kwargs``, furthermore most of the - methods like `get_sections` require ``**kwargs`` to be provided.""" + Parameters + ---------- + ``*args`` + Positional parameters that shall be used for the substitution. Note + that you can only provide either ``*args`` or ``**kwargs``, + furthermore most of the methods like `get_sections` require + ``**kwargs`` to be provided (if any). + ``**kwargs`` + Initial parameters to use + """ if len(args) and len(kwargs): raise ValueError("Only positional or keyword args are allowed") self.params = args or kwargs @@ -414,7 +417,7 @@ def __init__(self, *args, **kwargs): @updates_docstring def __call__(self, s): """ - Substitute in a docstring of a function with :attr:`params` + Substitute in a docstring of a function with :attr:`params`. Parameters ---------- @@ -425,16 +428,18 @@ def __call__(self, s): See Also -------- dedent: also dedents the doc - with_indent: also indents the doc""" + with_indent: also indents the doc + """ return safe_modulo(s, self.params, stacklevel=3) @reads_docstring def get_sections(self, s, base=None, sections=['Parameters', 'Other Parameters']): - """ - Method that extracts the specified sections out of the given string if - (and only if) the docstring follows the numpy documentation guidelines - [1]_. Note that the section either must appear in the + r"""Exctract sections out of a docstring. + + This method extracts the specified `sections` out of the given string + if (and only if) the docstring follows the numpy documentation + guidelines [1]_. Note that the section either must appear in the :attr:`param_like_sections` or the :attr:`text_sections` attribute. Parameters @@ -445,7 +450,7 @@ def get_sections(self, s, base=None, base to use in the :attr:`sections` attribute sections: list of str sections to look for. Each section must be followed by a newline - character ('\\n') and a bar of '-' (following the numpy (napoleon) + character ('\n') and a bar of '-' (following the numpy (napoleon) docstring conventions). Returns @@ -544,7 +549,7 @@ def with_indent(self, s, indent=0, stacklevel=3): def delete_params(self, base_key, *params): """ - Method to delete a parameter from a parameter documentation. + Delete a parameter from a parameter documentation. This method deletes the given `param` from the `base_key` item in the :attr:`params` dictionary and creates a new item with the original @@ -567,14 +572,15 @@ def delete_params(self, base_key, *params): See Also -------- - delete_types, keep_params""" + delete_types, keep_params + """ self.params[ base_key + '.no_' + '|'.join(params)] = delete_params( self.params[base_key], *params) def delete_kwargs(self, base_key, args=None, kwargs=None): """ - Deletes the ``*args`` or ``**kwargs`` part from the parameters section + Delete the ``*args`` or ``**kwargs`` part from the parameters section. Either `args` or `kwargs` must not be None. The resulting key will be stored in @@ -600,7 +606,8 @@ def delete_kwargs(self, base_key, args=None, kwargs=None): The type name of `args` in the base has to be like ````*```` (i.e. the `args` argument preceeded by a ``'*'`` and enclosed by double ``'`'``). Similarily, the type name of `kwargs` in `s` has to be like - ````**````""" + ````**```` + """ if not args and not kwargs: warn("Neither args nor kwargs are given. I do nothing for %s" % ( base_key)) @@ -612,7 +619,7 @@ def delete_kwargs(self, base_key, args=None, kwargs=None): def delete_types(self, base_key, out_key, *types): """ - Method to delete a parameter from a parameter documentation. + Delete a parameter from a parameter documentation. This method deletes the given `param` from the `base_key` item in the :attr:`params` dictionary and creates a new item with the original @@ -641,7 +648,7 @@ def delete_types(self, base_key, out_key, *types): def keep_params(self, base_key, *params): """ - Method to keep only specific parameters from a parameter documentation. + Keep only specific parameters from a parameter documentation. This method extracts the given `param` from the `base_key` item in the :attr:`params` dictionary and creates a new item with the original @@ -811,7 +818,7 @@ def keep_types(self, base_key, out_key, *types): @reads_docstring def get_docstring(self, s, base=None): - """get a docstring from a function. + """Get a docstring of a function. Like the :meth:`get_sections` method this method serves as a descriptor for functions but saves the entire docstring. From 8f03ab2337d74074ae8b8691b7c901ed571b6ef7 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Sun, 12 Jul 2020 10:50:31 +0200 Subject: [PATCH 15/20] combine deprecated_method and deprecated_function into on single decorator: deprecated --- docrep/__init__.py | 30 ++++++++-------- docrep/decorators.py | 86 +++++++++++++++++++------------------------- 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/docrep/__init__.py b/docrep/__init__.py index 8617b6f..b643eb6 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -5,7 +5,7 @@ from warnings import warn from docrep.decorators import ( - updates_docstring, reads_docstring, deprecated_method, deprecated_function) + updates_docstring, reads_docstring, deprecated) __version__ = '0.3.0.dev0' @@ -393,7 +393,7 @@ def __init__(self, *args, **kwargs): ``**kwargs`` Initial parameters to use """ - if len(args) and len(kwargs): + if args and kwargs: raise ValueError("Only positional or keyword args are allowed") self.params = args or kwargs patterns = {} @@ -918,61 +918,61 @@ def get_full_description(self, s, base=None): # ------------------ DEPRECATED METHODS ----------------------------------- - @deprecated_method('dedent', "0.3.0") + @deprecated('dedent', "0.3.0") def dedents(self, *args, **kwargs): pass - @deprecated_method('get_sections', "0.3.0", replace=False) + @deprecated('get_sections', "0.3.0", replace=False) def get_sectionsf(self, *args, **kwargs): return self.get_sections(base=args[0], *args[1:], **kwargs) - @deprecated_method('with_indent', "0.3.0") + @deprecated('with_indent', "0.3.0") def with_indents(self, *args, **kwargs): pass @staticmethod - @deprecated_function(_delete_params_s, "0.3.0", True, 'docrep.delete_params') + @deprecated(_delete_params_s, "0.3.0", True, 'docrep.delete_params') def delete_params_s(*args, **kwargs): pass @staticmethod - @deprecated_function(_delete_types_s, "0.3.0", True, 'docrep.delete_types') + @deprecated(_delete_types_s, "0.3.0", True, 'docrep.delete_types') def delete_types_s(*args, **kwargs): pass @classmethod - @deprecated_method(_delete_kwargs_s, "0.3.0", True, 'docrep.delete_kwargs') + @deprecated(_delete_kwargs_s, "0.3.0", True, 'docrep.delete_kwargs') def delete_kwargs_s(cls, *args, **kwargs): pass @staticmethod - @deprecated_function(_keep_params_s, "0.3.0", True, 'docrep.keep_params') + @deprecated(_keep_params_s, "0.3.0", True, 'docrep.keep_params') def keep_params_s(*args, **kwargs): pass @staticmethod - @deprecated_function(_keep_types_s, "0.3.0", True, 'docrep.keep_types') + @deprecated(_keep_types_s, "0.3.0", True, 'docrep.keep_types') def keep_types_s(*args, **kwargs): pass - @deprecated_method('get_docstring', "0.3.0", replace=False) + @deprecated('get_docstring', "0.3.0", replace=False) def save_docstring(self, *args, **kwargs): return self.get_docstring(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_summary', "0.3.0", replace=False) + @deprecated('get_summary', "0.3.0", replace=False) def get_summaryf(self, *args, **kwargs): return self.get_summary(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_full_description', "0.3.0", replace=False) + @deprecated('get_full_description', "0.3.0", replace=False) def get_full_descriptionf(self, *args, **kwargs): return self.get_full_description(base=args[0], *args[1:], **kwargs) - @deprecated_method('get_extended_summary', "0.3.0", replace=False) + @deprecated('get_extended_summary', "0.3.0", replace=False) def get_extended_summaryf(self, *args, **kwargs): return self.get_extended_summary(base=args[0], *args[1:], **kwargs) -@deprecated_function(inspect.cleandoc, "0.2.6", +@deprecated(inspect.cleandoc, "0.2.6", replacement_name='inspect.cleandoc') def dedents(s): pass \ No newline at end of file diff --git a/docrep/decorators.py b/docrep/decorators.py index 26d9b3a..6b2e9b5 100644 --- a/docrep/decorators.py +++ b/docrep/decorators.py @@ -51,6 +51,7 @@ def use_docstring(self, s=None, base=None, *args, **kwargs): def decorator(f): func(self, f.__doc__, base, *args, **kwargs) + return f return decorator else: @@ -59,8 +60,7 @@ def decorator(f): return use_docstring -def deprecated_method(replacement, version, replace=True, - replacement_name=None): +def deprecated(replacement, version, replace=True, replacement_name=None): """Mark a method as deprecated. Parameters @@ -80,62 +80,48 @@ def deprecated_method(replacement, version, replace=True, def decorate(func): - msg = "The %s method is deprecated, use the %s method instead" % ( - func.__name__, replacement_name) - - def deprecated(self, *args, **kwargs): - warn(msg, DeprecationWarning, stacklevel=2) - if callable(replacement) and replace: - return replacement(*args, **kwargs) - elif replace: - return getattr(self, replacement)(*args, **kwargs) - else: - return func(self, *args, **kwargs) - - deprecated.__doc__ = deprecated_doc.format( - type="method", version=version, replacement=replacement_name, - type_short="meth") - - return functools.wraps( - func, assigned=set(functools.WRAPPER_ASSIGNMENTS) - {'__doc__'})( - deprecated) - - return decorate - + try: + args = inspect.getfullargspec(func).args + except AttributeError: # py27 + args = inspect.getargspec(func).args -def deprecated_function(replacement, version, replace=True, - replacement_name=None): - """Mark a method as deprecated. + if args and args[0] in ['self', 'cls']: # assume classmethod or method + what = 'method' + else: + what = 'function' - Parameters - ---------- - replacement: callable - A function to call instead, in case `replace` is True - replace: bool - Whether to replace the call of the function with a call of - `replacement` - replacement_name: str - The name of the `replacement` function. If this is None, - `replacement.__name__` is used - """ + msg = "The {name} {type} is deprecated, use the {repl} {type} instead" + msg = msg.format(name=func.__name__, type=what, repl=replacement_name) - replacement_name = replacement_name or replacement.__name__ + if what == 'method': - def decorate(func): + def deprecated(self, *args, **kwargs): + warn(msg, DeprecationWarning, stacklevel=2) + if callable(replacement) and replace: + return replacement(*args, **kwargs) + elif replace: + return getattr(self, replacement)(*args, **kwargs) + else: + return func(self, *args, **kwargs) - msg = "The %s function is deprecated, use the %s method instead" % ( - func.__name__, replacement_name) + else: - def deprecated(*args, **kwargs): - warn(msg, DeprecationWarning, stacklevel=2) if replace: - return replacement(*args, **kwargs) - else: - return func(*args, **kwargs) + if not callable(replacement): + raise ValueError( + "Replacement functions for deprecated functions must " + "be callable!") + + def deprecated(*args, **kwargs): + warn(msg, DeprecationWarning, stacklevel=2) + if replace: + return replacement(*args, **kwargs) + else: + return func(*args, **kwargs) deprecated.__doc__ = deprecated_doc.format( - type="function", version=version, replacement=replacement_name, - type_short="func") + type=what, version=version, replacement=replacement_name, + type_short=what[:4]) return functools.wraps( func, assigned=set(functools.WRAPPER_ASSIGNMENTS) - {'__doc__'})( @@ -159,4 +145,4 @@ def _set_object_doc(obj, doc, stacklevel=3, py2_class='warn'): stacklevel=stacklevel) else: raise - return obj \ No newline at end of file + return obj From ec52516496a94eee510dc8fa65d7db9d392323ea Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Sun, 12 Jul 2020 11:11:35 +0200 Subject: [PATCH 16/20] add removed_in parameter for deprecated decorator --- docrep/decorators.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docrep/decorators.py b/docrep/decorators.py index 6b2e9b5..d14d45e 100644 --- a/docrep/decorators.py +++ b/docrep/decorators.py @@ -11,7 +11,7 @@ .. deprecated:: {version} - Use :{type_short}:`{replacement}` instead! + Use :{type_short}:`{replacement}` instead! {removed_in} """ @@ -60,7 +60,8 @@ def decorator(f): return use_docstring -def deprecated(replacement, version, replace=True, replacement_name=None): +def deprecated(replacement, version, replace=True, replacement_name=None, + removed_in=None): """Mark a method as deprecated. Parameters @@ -90,9 +91,13 @@ def decorate(func): else: what = 'function' - msg = "The {name} {type} is deprecated, use the {repl} {type} instead" + msg = "The {name} {type} is deprecated, use the {repl} {type} instead." msg = msg.format(name=func.__name__, type=what, repl=replacement_name) + if removed_in: + msg += " {name} will be removed in {removed_in}".format( + name=func.__name__, removed_in=removed_in) + if what == 'method': def deprecated(self, *args, **kwargs): @@ -121,7 +126,9 @@ def deprecated(*args, **kwargs): deprecated.__doc__ = deprecated_doc.format( type=what, version=version, replacement=replacement_name, - type_short=what[:4]) + type_short=what[:4], + removed_in=(("It will be removed in version {%s}." % removed_in) + if removed_in else "")) return functools.wraps( func, assigned=set(functools.WRAPPER_ASSIGNMENTS) - {'__doc__'})( From f5f8fdcc6e8153a9ef1fc84fb58b5318ea59fb59 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Sun, 12 Jul 2020 11:12:50 +0200 Subject: [PATCH 17/20] consider API changes for keep_params_s, etc. the call is now keep_params(s, *params), not anymore keep_params(s, params) --- docrep/__init__.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/docrep/__init__.py b/docrep/__init__.py index b643eb6..20a6218 100755 --- a/docrep/__init__.py +++ b/docrep/__init__.py @@ -240,11 +240,11 @@ def keep_types(s, *types): # assign delete_params a new name for the deprecation of the corresponding # DocstringProcessor method -_delete_params_s = delete_params -_delete_types_s = delete_types +_delete_params_s = lambda s, params: delete_params(s, *params) +_delete_types_s = lambda s, types: delete_types(s, *types) _delete_kwargs_s = delete_kwargs -_keep_params_s = keep_params -_keep_types_s = keep_types +_keep_params_s = lambda s, params: keep_params(s, *params) +_keep_types_s = lambda s, types: keep_types(s, *types) class DocstringProcessor(object): @@ -918,61 +918,68 @@ def get_full_description(self, s, base=None): # ------------------ DEPRECATED METHODS ----------------------------------- - @deprecated('dedent', "0.3.0") + @deprecated('dedent', "0.3.0", removed_in="0.4.0") def dedents(self, *args, **kwargs): pass - @deprecated('get_sections', "0.3.0", replace=False) + @deprecated('get_sections', "0.3.0", replace=False, removed_in="0.4.0") def get_sectionsf(self, *args, **kwargs): return self.get_sections(base=args[0], *args[1:], **kwargs) - @deprecated('with_indent', "0.3.0") + @deprecated('with_indent', "0.3.0", removed_in="0.4.0") def with_indents(self, *args, **kwargs): pass @staticmethod - @deprecated(_delete_params_s, "0.3.0", True, 'docrep.delete_params') + @deprecated(_delete_params_s, "0.3.0", True, 'docrep.delete_params', + removed_in="0.4.0") def delete_params_s(*args, **kwargs): pass @staticmethod - @deprecated(_delete_types_s, "0.3.0", True, 'docrep.delete_types') + @deprecated(_delete_types_s, "0.3.0", True, 'docrep.delete_types', + removed_in="0.4.0") def delete_types_s(*args, **kwargs): pass @classmethod - @deprecated(_delete_kwargs_s, "0.3.0", True, 'docrep.delete_kwargs') + @deprecated(_delete_kwargs_s, "0.3.0", True, 'docrep.delete_kwargs', + removed_in="0.4.0") def delete_kwargs_s(cls, *args, **kwargs): pass @staticmethod - @deprecated(_keep_params_s, "0.3.0", True, 'docrep.keep_params') + @deprecated(_keep_params_s, "0.3.0", True, 'docrep.keep_params', + removed_in="0.4.0") def keep_params_s(*args, **kwargs): pass @staticmethod - @deprecated(_keep_types_s, "0.3.0", True, 'docrep.keep_types') + @deprecated(_keep_types_s, "0.3.0", True, 'docrep.keep_types', + removed_in="0.4.0") def keep_types_s(*args, **kwargs): pass - @deprecated('get_docstring', "0.3.0", replace=False) + @deprecated('get_docstring', "0.3.0", replace=False, removed_in="0.4.0") def save_docstring(self, *args, **kwargs): return self.get_docstring(base=args[0], *args[1:], **kwargs) - @deprecated('get_summary', "0.3.0", replace=False) + @deprecated('get_summary', "0.3.0", replace=False, removed_in="0.4.0") def get_summaryf(self, *args, **kwargs): return self.get_summary(base=args[0], *args[1:], **kwargs) - @deprecated('get_full_description', "0.3.0", replace=False) + @deprecated('get_full_description', "0.3.0", replace=False, + removed_in="0.4.0") def get_full_descriptionf(self, *args, **kwargs): return self.get_full_description(base=args[0], *args[1:], **kwargs) - @deprecated('get_extended_summary', "0.3.0", replace=False) + @deprecated('get_extended_summary', "0.3.0", replace=False, + removed_in="0.4.0") def get_extended_summaryf(self, *args, **kwargs): return self.get_extended_summary(base=args[0], *args[1:], **kwargs) -@deprecated(inspect.cleandoc, "0.2.6", - replacement_name='inspect.cleandoc') +@deprecated(inspect.cleandoc, "0.2.6", replacement_name='inspect.cleandoc', + removed_in="0.4.0") def dedents(s): pass \ No newline at end of file From a907627aff37c763eb5aef5d57e9fcd40da4a2b7 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Wed, 26 Aug 2020 22:33:25 +0200 Subject: [PATCH 18/20] Add section about the docrep workflow --- docs/index.rst | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 341e349..d24feea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -221,6 +221,47 @@ example .. _`numpy conventions`: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +The docrep workflow +=================== + +The general workflow is: + +1. Create an instance of the :class:`DocstringProcessor`:: + + >>> from docrep import DocstringProcessor + >>> docstrings = DocstringProcessor() + +2. Analyse the docstring of a function, class or method:: + + >>> @docstrings.get_sections + ... def my_function(...): + ... """...""" + + Available methods for analysing the docstring are: + + .. autoclasssumm:: DocstringProcessor + :autosummary-sections: Analysis Methods + :autosummary-no-titles: + +3. Optionally process the docstring using one of the analysis methods + + .. autoclasssumm:: DocstringProcessor + :autosummary-sections: Extraction Methods + :autosummary-no-titles: + +4. Reuse the docstring somewhere else with one of the update methods: + + .. autoclasssumm:: DocstringProcessor + :autosummary-sections: Updating Methods + :autosummary-no-titles: + + For instance via:: + + >>> @docstrings.dedent + ... def my_other_function(...): + ... """...""" + + Installation ============= Installation simply goes via pip:: From 5bf3af3b86c967a4175d8d2cd831bac7385f2d7d Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Wed, 26 Aug 2020 22:47:11 +0200 Subject: [PATCH 19/20] Minor fix for tests --- tests/test_docrep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_docrep.py b/tests/test_docrep.py index 9b9e3e4..1f8d876 100644 --- a/tests/test_docrep.py +++ b/tests/test_docrep.py @@ -705,7 +705,7 @@ def test_keep_params_s(self): joined_pdescs = '\n'.join(pdescs) ds = docrep.DocstringProcessor() with self.assertWarnsRegex(DeprecationWarning, 'keep_params_s'): - txt = ds.keep_params_s('\n'.join(all_pdescs), *params) + txt = ds.keep_params_s('\n'.join(all_pdescs), params) # check single self.assertEqual(txt, joined_pdescs, msg='Wrong description for params {}'.format(params)) From e849ee246d05561436b28568caf02996993c7b85 Mon Sep 17 00:00:00 2001 From: "Philipp S. Sommer" Date: Wed, 26 Aug 2020 22:48:11 +0200 Subject: [PATCH 20/20] add link to PR in changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34c386b..c7b1f73 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,6 @@ v0.3.0 ====== -New framework for decorators +New framework for decorators, see PR `#19 `__ This release deprecates several methods of the :class:`DocstringProcessor` in favor of a more uniform framework. Functions such as `get_sections` and `dedent`