#!/usr/bin/env python3
#
# autoattrs.py
"""
Sphinx directive for documenting attrs_ classes.
.. _attrs: https://www.attrs.org/en/stable/
.. versionadded:: 0.1.0
.. attention::
Due to changes in the :mod:`typing` module :mod:`~attr_utils.autoattrs`
is only officially supported on Python 3.7 and above.
.. extras-require:: sphinx
:pyproject:
.. rst:directive:: autoattrs
Autodoc directive to document an `attrs <https://www.attrs.org/>`__ class.
It behaves much like :rst:dir:`autoclass`. It can be used directly or as part of :rst:dir:`automodule`.
Docstrings for parameters in ``__init__`` can be given in the class docstring or alongside each attribute
(see :rst:dir:`autoattribute` for the syntax). The second option is recommended as it interacts better
with other parts of autodoc. However, the class docstring can be used to override the description
for a given parameter.
.. rst:directive:option:: no-init-attribs
:type: flag
Excludes attributes taken as arguments in ``__init__`` from the output, even if they are documented.
This may be useful for simple classes where converter functions aren't used.
This option cannot be used as part of :rst:dir:`automodule`.
.. latex:clearpage::
API Reference
---------------
.. latex:vspace:: -20px
"""
#
# Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# Parts based on https://github.com/sphinx-doc/sphinx
# | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file).
# | BSD Licensed
# | All rights reserved.
# |
# | Redistribution and use in source and binary forms, with or without
# | modification, are permitted provided that the following conditions are
# | met:
# |
# | * Redistributions of source code must retain the above copyright
# | notice, this list of conditions and the following disclaimer.
# |
# | * Redistributions in binary form must reproduce the above copyright
# | notice, this list of conditions and the following disclaimer in the
# | documentation and/or other materials provided with the distribution.
# |
# | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# stdlib
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Optional, Tuple, Type
# 3rd party
import attr
from sphinx.application import Sphinx # nodep
from sphinx.ext.autodoc import ClassDocumenter, Documenter # nodep
from sphinx.pycode import ModuleAnalyzer # nodep
from sphinx_toolbox import __version__ # nodep
from sphinx_toolbox.more_autosummary import PatchedAutoSummClassDocumenter # nodep
from sphinx_toolbox.utils import Param, SphinxExtMetadata, flag, parse_parameters, unknown_module_warning # nodep
# this package
from attr_utils.docstrings import add_attrs_doc
from attr_utils.utils import AttrsClass
__all__ = ["AttrsDocumenter", "setup"]
if TYPE_CHECKING:
# 3rd party
from docutils.statemachine import StringList # type: ignore[import]
def _documenter_add_content(
self: Documenter,
more_content: Optional["StringList"],
) -> None:
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
if self.analyzer:
if self.objpath:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
attr_docs = self.analyzer.find_attr_docs()
if key in attr_docs:
for i, line in enumerate(self.process_doc([list(attr_docs[key])])):
self.add_line(line, sourcename, i)
# add additional content (e.g. from document), if present
if more_content:
for line, src in zip(more_content.data, more_content.items):
self.add_line(line, src[0], src[1])
[docs]class AttrsDocumenter(PatchedAutoSummClassDocumenter):
r"""
Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter`
for documenting `attrs <https://www.attrs.org/>`__ classes.
.. versionchanged:: 0.3.0
* Parameters for ``__init__`` can be documented either in the class docstring
or alongside the attribute.
The class docstring has priority.
* Added support for `autodocsumm <https://github.com/Chilipp/autodocsumm>`_.
.. autosummary-widths:: 29/64
:html: 4/10
""" # noqa: D400
objtype = "attrs"
directivetype = "class"
priority = ClassDocumenter.priority + 1
option_spec = {
**PatchedAutoSummClassDocumenter.option_spec,
"no-init-attribs": flag,
}
object: Type[AttrsClass] # noqa: A003 # pylint: disable=redefined-builtin
[docs] @classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool:
"""
Called to see if a member can be documented by this documenter.
:param member:
:param membername:
:param isattr:
:param parent:
:rtype:
.. latex:clearpage::
"""
return attr.has(member) and isinstance(member, type)
[docs] def add_content(self, more_content: Any, no_docstring: bool = False) -> None: # type: ignore
"""
Add extra content (from docstrings, attribute docs etc.), but not the class docstring.
:param more_content:
:param no_docstring:
"""
_documenter_add_content(self, more_content)
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
self.add_line('', sourcename)
if hasattr(self.object, "__attrs_init__"):
# Size varies depending on docutils config
tab_size = self.env.app.config.docutils_tab_width
if self.object.__doc__:
docstring = dedent(self.object.__doc__).expandtabs(tab_size).split('\n')
for line in list(self.process_doc([docstring])):
self.add_line(line, sourcename)
else:
params, pre_output, post_output = self._get_docstring()
for line in list(self.process_doc([pre_output])):
self.add_line(line, sourcename)
self.add_line('', sourcename)
def _get_docstring(self) -> Tuple[Dict[str, Param], List[str], List[str]]:
"""
Returns params, pre_output, post_output.
"""
# Size varies depending on docutils config
tab_size = self.env.app.config.docutils_tab_width
if self.object.__doc__:
docstring = dedent(self.object.__doc__).expandtabs(tab_size).split('\n')
else:
docstring = []
return parse_parameters(docstring, tab_size=tab_size)
[docs] def import_object(self, raiseerror: bool = False) -> bool:
"""
Import the object given by ``self.modname`` and ``self.objpath`` and set it as ``self.object``.
If the object is an `attrs <https://www.attrs.org/>`__ class
:func:`attr_utils.docstrings.add_attrs_doc` will be called.
:param raiseerror:
:return: :py:obj:`True` if successful, :py:obj:`False` if an error occurred.
"""
ret = super().import_object(raiseerror)
if attr.has(self.object):
self.object = add_attrs_doc(self.object)
return ret
[docs] def sort_members(
self,
documenters: List[Tuple[Documenter, bool]],
order: str,
) -> List[Tuple[Documenter, bool]]:
"""
Sort the given member list and add attribute docstrings to the class docstring.
:param documenters:
:param order:
"""
# The documenters for the fields and methods, in the desired order
# The fields will be in bysource order regardless of the order option
documenters = super().sort_members(documenters, order)
if hasattr(self, "_docstring_processed"):
return documenters
# Mapping of member names to docstrings (as list of strings)
member_docstrings = {
k[1]: v
for k, v in ModuleAnalyzer.for_module(self.object.__module__).find_attr_docs().items()
}
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
parameter_docs = []
params, pre_output, post_output = self._get_docstring()
all_docs = {}
for field in (a.name for a in attr.fields(self.object) if a.init):
doc: List[str] = ['']
# Prefer doc from class docstring
if field in params:
doc, arg_type = params.pop(field).values() # type: ignore
# Otherwise use attribute docstring
if not ''.join(doc).strip() and field in member_docstrings:
doc = member_docstrings[field]
field_entry = [f":param {field}:", *doc]
parameter_docs.append(' '.join(field_entry))
all_docs[field] = ''.join(doc).strip()
if not hasattr(self.object, "__attrs_init__"):
self.add_line('', sourcename)
for line in self.process_doc([[*pre_output, *parameter_docs, '', '', *post_output]]):
if line and line in pre_output:
continue
self.add_line(line, sourcename)
self.add_line('', sourcename)
self._docstring_processed = True
if hasattr(self.object, "__slots__"):
slots_dict: MutableMapping[str, Optional[str]] = {}
for item in self.object.__slots__: # type: ignore[attr-defined]
if item in all_docs:
slots_dict[item] = all_docs[item]
else:
slots_dict[item] = None
self.object.__slots__ = slots_dict # type: ignore[attr-defined]
if hasattr(self, "add_autosummary"):
self.add_autosummary()
# import functools
#
# for documenter in documenters:
# documenter[0].parse_name()
# if documenter[0].objpath[-1] in all_docs:
# def get_doc(encoding: str = None, ignore: int = None, doc: str = '') -> List[List[str]]:
# return [[doc]]
# documenter[0].get_doc = functools.partial(get_doc, doc=all_docs[documenter[0].objpath[-1]])
return documenters
[docs] def filter_members(
self,
members: List[Tuple[str, Any]],
want_all: bool,
) -> List[Tuple[str, Any, bool]]:
"""
Filter the list of members to always include init attributes unless the
``:no-init-attribs:`` flag was given.
:param members:
:param want_all:
""" # noqa: D400
attrib_names = (a.name for a in attr.fields(self.object) if a.init)
no_init_attribs = self.options.get("no-init-attribs", False)
def unskip_attrs(app, what, name, obj, skip, options):
if skip and not no_init_attribs:
return not (name in attrib_names)
elif no_init_attribs and (name in attrib_names):
return True
return None
listener_id = self.env.app.connect("autodoc-skip-member", unskip_attrs)
members_ = super().filter_members(members, want_all)
self.env.app.disconnect(listener_id)
return members_
[docs] def generate(
self,
more_content: Optional[Any] = None,
real_modname: Optional[str] = None,
check_module: bool = False,
all_members: bool = False,
) -> None:
"""
Generate reST for the object given by ``self.name``, and possibly for its members.
:param more_content: Additional content to include in the reST output.
:param real_modname: Module name to use to find attribute documentation.
:param check_module: If :py:obj:`True`, only generate if the object is defined
in the module name it is imported from.
:param all_members: If :py:obj:`True`, document all members.
.. latex:vspace:: -6px
"""
if not self.parse_name(): # pragma: no cover
# need a module to import
unknown_module_warning(self)
return None
# now, import the module and get object to document
if not self.import_object():
return None # pragma: no cover
return super().generate(
more_content=more_content,
real_modname=real_modname,
check_module=check_module,
all_members=all_members,
)
[docs]def setup(app: Sphinx) -> SphinxExtMetadata:
"""
Setup :mod:`attr_utils.autoattrs`.
:param app:
"""
# Hack to get the docutils tab size, as there doesn't appear to be any other way
app.setup_extension("sphinx_toolbox.tweaks.tabsize")
app.setup_extension("sphinx_toolbox.more_autosummary")
app.add_autodocumenter(AttrsDocumenter)
return {
"version": __version__,
"parallel_read_safe": True,
}