Source code for deprecat.sphinx

"""
Sphinx directive integration
============================

The purpose of this module is to defined decorators which adds this Sphinx directives
to the docstring of your function and classes. Additionally, the ``@deprecat`` decorator will emit a deprecation warning
when the function/method is called or the class is constructed.

We usually need to document the life-cycle of functions and classes:
when they are created, modified or deprecated. To do that, `Sphinx <http://www.sphinx-doc.org>`_ has a set
of `Paragraph-level markups <http://www.sphinx-doc.org/en/stable/markup/para.html>`_:

- ``deprecated``: to document a deprecated feature.

"""
import re
import textwrap
import functools
import wrapt
import warnings

from deprecat.classic import ClassicAdapter
from deprecat.classic import deprecat as _classic_deprecat

try:
    # If the C extension for wrapt was compiled and wrapt/_wrappers.pyd exists, then the
    # stack level that should be passed to warnings.warn should be 2. However, if using
    # a pure python wrapt, a extra stacklevel is required.
    import wrapt._wrappers

    _routine_stacklevel = 2
    _class_stacklevel = 2
except ImportError:
    _routine_stacklevel = 3
    if platform.python_implementation() == "PyPy":
        _class_stacklevel = 2
    else:
        _class_stacklevel = 3
        
[docs] class SphinxAdapter(ClassicAdapter): """ Construct a wrapper adapter. Parameters ---------- reason: str Reason for deprecation. version: str Version of your project which deprecates this feature. remove_version: str Version of your project which removes this method or class. action: str A warning filter used to specify the deprecation warning. Can be one of "error", "ignore", "always", "default", "module", or "once". If ``None`` or empty, the the global filtering mechanism is used. deprecated_args: dict Dictionary in the following format to deprecate `x` and `y` deprecated_args = {'x': {'reason': 'some reason','version': '1.0'},'y': {'reason': 'another reason','version': '2.0'}} category: class The warning category to use for the deprecation warning. By default, the category class is :class:`~DeprecationWarning`, you can inherit this class to define your own deprecation warning category. line_length: numeric Max line length of the directive text. If non null, a long text is wrapped in several lines. directive: {"versionadded", "versionchanged", "deprecated"} Sphinx directive Notes ----- Sphinx adapter overrides the :class:`~deprecat.classic.ClassicAdapter` in order to add the Sphinx directives to the end of the function/class docstring. Such a directive is a `Paragraph-level markup <http://www.sphinx-doc.org/en/stable/markup/para.html>`_ * The directive can be one of "versionadded", "versionchanged" or "deprecated". * The version number is added if provided. * The reason message is added in the directive block if not empty. .. seealso:: We use admonitions in sphinx to render warnings for every deprecated argument just below its description in docstring. refer to `this <https://pradyunsg.me/furo/reference/admonitions/?highlight=warning#custom-titles>`_ link for more information. Warnings -------- deprecat supports docstring modification for deprecated_args only in the numpydoc format, if your documentation uses any other format, this won't work. Later we might add support for other formats, for now there are no such plans. """ def __init__( self, directive, reason="", version="", remove_version="", action=None, category=DeprecationWarning, line_length=70, deprecated_args=None ): self.deprecated_args = deprecated_args self.directive = directive self.line_length = line_length super(SphinxAdapter, self).__init__(reason=reason, version=version, remove_version=remove_version, action=action, category=category, deprecated_args=deprecated_args)
[docs] def __call__(self, wrapped): """ Add the Sphinx directive to your class or function. Parameters ---------- wrapped: object Wrapped class or function. Returns ------- the decorated class or function. """ docstring = textwrap.dedent(wrapped.__doc__ or "") if docstring: # An empty line must separate the original docstring and the directive. docstring = re.sub(r"\n+$", "", docstring, flags=re.DOTALL) + "\n\n" else: # Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring docstring = "\n" width = self.line_length - 3 if self.line_length > 3 else 2 ** 16 reason = self.reason if self.remove_version!="": reason += f'\n\nWarning: This deprecated feature will be removed in version {self.remove_version}' reason = textwrap.dedent(reason).strip() if self.deprecated_args is None: fmt = ".. {directive}:: {version}" if self.version else ".. {directive}::" div_lines = [fmt.format(directive=self.directive, version=self.version)] #formatting for docstring for paragraph in reason.splitlines(): if paragraph: div_lines.extend( textwrap.fill( paragraph, width=width, initial_indent=" ", subsequent_indent=" ", ).splitlines() ) else: div_lines.append("") # -- append the directive division to the docstring docstring += "".join("{}\n".format(line) for line in div_lines) else: if docstring=="\n": warnings.warn("Missing docstring, consider adding a numpydoc style docstring for the decorator to work (Sphinx directive won't be added)" , category=UserWarning, stacklevel=_class_stacklevel) else: for arg in set(self.deprecated_args.keys()): #first we search for the location of the parameters section search = re.search("Parameters[\\s]*\n[\\s]*----------", docstring) if search is None: warnings.warn("Missing Parameter section, consider adding a numpydoc style parameters section in your docstring for the decorator to work (Sphinx directive won't be added)" , category=UserWarning, stacklevel=_class_stacklevel) else: params_string = docstring[search.start():search.end()] #we store the indentation of the values indentsize = re.search("----------", params_string).start() - re.search("Parameters[\\s]*\n", params_string).end() indent = ' '*indentsize # we check if there is another section after parameters if re.search(f"\n{indent}-----", docstring[search.end():]) is not None: #if yes then we find the range of the parameters section params_section_end = search.end() + re.search(f"\n{indent}-----", docstring[search.end():]).start() dashes_in_next_section = docstring[params_section_end:].count('-') params_section_end = params_section_end - dashes_in_next_section params_section = docstring[search.start():params_section_end] else: #else the entire remaining docstring is in the parameters section params_section = docstring[search.start():] #we search for the description of the particular parameter we care about if re.search(f"\n{indent}{arg}\\s*:", params_section) is not None: description_start = re.search(f"\n{indent}{arg}\\s*:", params_section).end() #we check whether there are more parameters after this one, or if its the last parameter described in the secion #and store the position where we insert the warning if re.search(f"\n{indent}\\S", params_section[description_start:]): insert_pos = re.search(f"\n{indent}\\S", params_section[description_start:]).start() else: insert_pos = len(params_section[description_start:]) #finally we store the warning fmt string if self.deprecated_args[arg].get('version') is not None: #the spaces are specifically cherrypicked for numpydoc docstrings fmt = "\n\n .. admonition:: Deprecated\n :class: warning\n\n Parameter {arg} deprecated since {version}" if self.deprecated_args[arg].get('remove_version') is not None: fmt += " and will be removed in version {remove_version}." div_lines = [fmt.format(version=self.deprecated_args[arg]['version'],arg=arg, remove_version=self.deprecated_args[arg]['remove_version'])] else: div_lines = [fmt.format(version=self.deprecated_args[arg]['version'],arg=arg)] else: fmt = "\n\n .. admonition:: Deprecated\n :class: warning\n\n Parameter {arg} deprecated" div_lines = [fmt.format(version=self.deprecated_args[arg]['version'],arg=arg)] width = 2**16 if self.remove_version!="": self.reason += f'\n\nWarning: This deprecated feature will be removed in version {self.remove_version}' reason = textwrap.dedent(self.reason).strip() #formatting for docstring for paragraph in reason.splitlines(): div_lines.extend( textwrap.fill( paragraph, width=width, initial_indent=indent, subsequent_indent=indent, ).splitlines() ) # -- append the directive division to the docstring a='' a += "".join("{}\n".format(line) for line in div_lines) a = textwrap.indent(a, indent) docstring = docstring[:search.start() + description_start+insert_pos]+"\n\n"+a+"\n\n"+docstring[search.start() + description_start+insert_pos:] docstring = re.sub(r"[\n]{3,}", "\n\n", docstring) else: warnings.warn(f"Missing description for parameter {arg}, consider adding a numpydoc style description for the decorator to work (Sphinx directive won't be added)" , category=UserWarning, stacklevel=_class_stacklevel) wrapped.__doc__ = docstring if self.directive in {"versionadded", "versionchanged"}: return wrapped return super(SphinxAdapter, self).__call__(wrapped)
[docs] def get_deprecated_msg(self, wrapped, instance, kwargs): """ Get the deprecation warning message (without Sphinx cross-referencing syntax) for the user. Parameters ---------- wrapped: Wrapped class or function. instance: The object to which the wrapped function was bound when it was called. kwargs: The kwargs of the wrapped function. Returns ------- The warning message. """ msg = super(SphinxAdapter, self).get_deprecated_msg(wrapped, instance, kwargs) # Strip Sphinx cross reference syntax (like ":function:", ":py:func:" and ":py:meth:") # Possible values are ":role:`foo`", ":domain:role:`foo`" # where ``role`` and ``domain`` should match "[a-zA-Z]+" #remember the msg variable is a dict if msg: for key, value in msg.items(): msg[key] = re.sub(r"(?: : [a-zA-Z]+ )? : [a-zA-Z]+ : (`[^`]*`)", r"\1", value, flags=re.X) return msg
#this should only be used for functions, methods or classes. NOT parameters.
[docs] def versionadded(reason="", version="", line_length=70): """ This decorator can be used to insert a "versionadded" directive in your function/class docstring in order to documents the version of the project which adds this new functionality in your library. Parameters ---------- reason: str Reason for deprecation of this method or class. version: str Version of your project which deprecates this method or class. line_length: numeric Max line length of the directive text. If non null, a long text is wrapped in several lines. Returns ------- Decorator used to modify docstring. """ adapter = SphinxAdapter( 'versionadded', reason=reason, version=version, line_length=line_length, ) return adapter
#this should only be used for functions, methods or classes. NOT parameters.
[docs] def versionchanged(reason="", version="", line_length=70): """ This decorator can be used to insert a "versionchanged" directive in your function/class docstring in order to documents the version of the project which modifies this functionality in your library. Parameters ---------- reason: str Reason for deprecation of this method or class. version: str Version of your project which deprecates this method or class. line_length: numeric Max line length of the directive text. If non null, a long text is wrapped in several lines. Returns ------- Decorator used to modify docstring. """ adapter = SphinxAdapter( 'versionchanged', reason=reason, version=version, line_length=line_length, ) return adapter
[docs] def deprecat(reason="", directive="deprecated", version="", remove_version="", line_length=70, deprecated_args=None, **kwargs): """ This decorator can be used to insert a "deprecated" directive in your function/class docstring in order to documents the version of the project which deprecates this functionality in your library. Parameters ---------- reason: str Reason for deprecation of this method or class. directive: str Sphinx directive to use, defaults to "deprecated". version: str Version of your project which deprecates this method or class. remove_version: str Version of your project which removes this method or class. line_length: numeric Max line length of the directive text. If non null, a long text is wrapped in several lines. category: class The warning category to use for the deprecation warning. By default, the category class is :class:`~DeprecationWarning`, you can inherit this class to define your own deprecation warning category. deprecated_args: dict Dictionary in the following format to deprecate `x` and `y` deprecated_args = {'x': {'reason': 'some reason','version': '1.0'},'y': {'reason': 'another reason', 'version': '2.0'}} action: str A warning filter used to specify the deprecation warning. Can be one of "error", "ignore", "always", "default", "module", or "once". If ``None`` or empty, the the global filtering mechanism is used. Returns ------- Decorator used to deprecate a function, method, class or kwarg. """ directive = kwargs.pop('directive', 'deprecated') adapter_cls = kwargs.pop('adapter_cls', SphinxAdapter) kwargs["reason"] = reason kwargs["version"] = version kwargs["remove_version"] = remove_version kwargs["line_length"] = line_length kwargs["deprecated_args"] = deprecated_args return _classic_deprecat(directive=directive, adapter_cls=adapter_cls, **kwargs)