Source code for sphinx_toolbox.more_autosummary

#!/usr/bin/env python3
#
#  __init__.py
r"""
Extensions to :mod:`sphinx.ext.autosummary`.

Provides an enhanced version of https://autodocsumm.readthedocs.io/
which respects the autodoc ``member-order`` option.
This can be given for an individual directive, in the
`autodoc_member_order <https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_member_order>`_
configuration value, or via :confval:`autodocsumm_member_order`.

Also patches :class:`sphinx.ext.autosummary.Autosummary` to fix an issue where
the module name is sometimes duplicated.
I.e. ``foo.bar.baz()`` became ``foo.bar.foo.bar.baz()``, which of course doesn't exist
and created a broken link.


.. versionadded:: 0.7.0

.. versionchanged:: 1.3.0

	Autosummary now selects the appropriate documenter for attributes rather than
	falling back to :class:`~sphinx.ext.autodoc.DataDocumenter`.

.. versionchanged:: 2.13.0

	Also patches :class:`sphinx.ext.autodoc.ModuleDocumenter` to fix an issue where
	``__all__`` is not respected for autosummary tables.



Configuration
--------------

.. latex:vspace:: -20px

.. confval:: autodocsumm_member_order
	:type: :py:obj:`str`
	:default: ``'alphabetical'``

	Determines the sort order of members in ``autodocsumm`` summary tables.
	Valid values are ``'alphabetical'`` and ``'bysource'``.

	Note that for ``'bysource'`` the module must be a Python module with the source code available.

	The member order can also be set on a per-directive basis using the ``:member-order: [order]`` option.
	This applies not only to :rst:dir:`automodule` etc. directives,
	but also to :rst:dir:`automodulesumm` etc. directives.

.. confval:: autosummary_col_type
	:type: :py:obj:`str`
	:default: ``'\X'``

	The LaTeX column type to use for autosummary tables.

	Custom columns can be defined in the LaTeX preamble for use with this option.

	For example:

	.. code-block:: python

		latex_elements["preamble"] = r'''
			\makeatletter
			\newcolumntype{\Xx}[2]{>{\raggedright\arraybackslash}p{\dimexpr
				(\linewidth-\arrayrulewidth)*#1/#2-\tw@\tabcolsep-\arrayrulewidth\relax}}
			\makeatother
			'''

		autosummary_col_type = "\\Xx"


	.. versionadded:: 2.13.0



API Reference
----------------

"""
#
#  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.
#
#  Builds on top of, and PatchedAutoDocSummDirective based on, https://github.com/Chilipp/autodocsumm
#  | Copyright 2016-2019, Philipp S. Sommer
#  | Copyright 2020-2021, Helmholtz-Zentrum Hereon
#  |
#  | Licensed under the Apache License, Version 2.0 (the "License");
#  | you may not use this file except in compliance with the License.
#  | You may obtain a copy of the License at
#  |
#  |     http://www.apache.org/licenses/LICENSE-2.0
#  |
#  | Unless required by applicable law or agreed to in writing,
#  | software distributed under the License is distributed on an "AS IS" BASIS,
#  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  | See the License for the specific language governing permissions and limitations under the License.
#

# stdlib
import inspect
import operator
import re
from typing import Any, Dict, List, Optional, Tuple, Type

# 3rd party
import autodocsumm  # type: ignore[import-untyped]
import docutils
import sphinx
from docutils import nodes
from domdf_python_tools.stringlist import StringList
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.config import ENUM
from sphinx.ext.autodoc import (
		ALL,
		INSTANCEATTR,
		ClassDocumenter,
		Documenter,
		ModuleDocumenter,
		logger,
		special_member_re
		)
from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options
from sphinx.ext.autosummary import Autosummary, FakeDirective, autosummary_table
from sphinx.locale import __
from sphinx.util.inspect import getdoc, safe_getattr

# this package
from sphinx_toolbox._data_documenter import DataDocumenter
from sphinx_toolbox.more_autodoc import ObjectMembers
from sphinx_toolbox.utils import SphinxExtMetadata, allow_subclass_add, get_first_matching, metadata_add_version

if sphinx.version_info > (4, 1):
	# 3rd party
	from sphinx.util.docstrings import separate_metadata
else:
	# 3rd party
	from sphinx.util.docstrings import extract_metadata  # type: ignore[attr-defined]

__all__ = (
		"PatchedAutosummary",
		"PatchedAutoSummModuleDocumenter",
		"PatchedAutoSummClassDocumenter",
		"get_documenter",
		"setup",
		)

if sphinx.version_info < (3, 5):
	# 3rd party
	from sphinx.ext.autodoc.importer import get_module_members as _get_module_members  # type: ignore[attr-defined]
else:  # pragma: no cover
	# 3rd party
	from sphinx.util.inspect import getannotations

	def _get_module_members(module: Any) -> List[Tuple[str, Any]]:
		"""Get members of target module."""
		# 3rd party
		from sphinx.ext.autodoc import INSTANCEATTR

		members: Dict[str, Tuple[str, Any]] = {}
		for name in dir(module):
			try:
				value = safe_getattr(module, name, None)
				members[name] = (name, value)
			except AttributeError:
				continue

		# annotation only member (ex. attr: int)
		for name in getannotations(module):
			if name not in members:
				members[name] = (name, INSTANCEATTR)

		return sorted(list(members.values()))


def add_autosummary(self, relative_ref_paths: bool = False) -> None:
	"""
	Add the :rst:dir:`autosummary` table of this documenter.

	:param relative_ref_paths: Use paths relative to the current module
		instead of absolute import paths for each object.
	"""

	if not self.options.get("autosummary", False):
		return

	content = StringList()
	content.indent_type = ' ' * 4
	sourcename = self.get_sourcename()
	grouped_documenters = self.get_grouped_documenters()

	if not self.options.get("autosummary-no-titles", False) and grouped_documenters:
		content.blankline()
		content.append(".. latex:vspace:: 10px")
		content.blankline()

	member_order = get_first_matching(
			lambda x: x != "groupwise",
			(
					self.options.get("member-order", ''),
					self.env.config.autodocsumm_member_order,
					self.env.config.autodoc_member_order,
					),
			default="alphabetical",
			)

	for section, documenters in grouped_documenters.items():
		if not self.options.get("autosummary-no-titles", False):
			content.append(f"**{section}:**")
			content.blankline()
			content.append(".. latex:vspace:: -5px")

		content.blankline(ensure_single=True)

		# TODO transform to make caption associated with table in LaTeX

		content.append(".. autosummary::")

		if self.options.autosummary_nosignatures:
			content.append("    :nosignatures:")

		content.blankline(ensure_single=True)

		with content.with_indent_size(content.indent_size + 1):
			for documenter, _ in self.sort_members(documenters, member_order):
				obj_ref_path = documenter.fullname

				# if relative_ref_paths:
				# 	modname = self.modname + '.'
				# 	if documenter.fullname.startswith(modname):
				# 		obj_ref_path = documenter.fullname[len(modname):]

				content.append(f"~{obj_ref_path}")

		content.blankline()

	for line in content:
		self.add_line(line, sourcename)


[docs]class PatchedAutosummary(Autosummary): """ Pretty table containing short signatures and summaries of functions etc. Patched version of :class:`sphinx.ext.autosummary.Autosummary` to fix an issue where the module name is sometimes duplicated. I.e. ``foo.bar.baz()`` became ``foo.bar.foo.bar.baz()``, which of course doesn't exist and created a broken link. .. versionadded:: 0.5.1 .. versionchanged:: 0.7.0 Moved from :mod:`sphinx_toolbox.patched_autosummary`. .. versionchanged:: 2.13.0 Added support for customising the column type with the :confval:`autosummary_col_type` option. """
[docs] def import_by_name(self, name: str, prefixes: List[Optional[str]]) -> Tuple[str, Any, Any, str]: """ Import the object with the give name. :param name: :param prefixes: :return: The real name of the object, the object, the parent of the object, and the name of the module. """ real_name, obj, parent, modname = super().import_by_name(name=name, prefixes=prefixes) real_name = re.sub(rf"((?:{modname}\.)+)", f"{modname}.", real_name) return real_name, obj, parent, modname
[docs] def create_documenter( self, app: Sphinx, obj: Any, parent: Any, full_name: str, ) -> Documenter: """ Get an :class:`autodoc.Documenter` class suitable for documenting the given object. :param app: The Sphinx application. :param obj: The object being documented. :param parent: The parent of the object (e.g. a module or a class). :param full_name: The full name of the object. .. versionchanged:: 1.3.0 Now selects the appropriate documenter for attributes rather than falling back to :class:`~sphinx.ext.autodoc.DataDocumenter`. """ doccls = get_documenter(app, obj, parent) return doccls(self.bridge, full_name)
[docs] def get_table(self, items: List[Tuple[str, str, str, str]]) -> List[nodes.Node]: """ Generate a list of table nodes for the :rst:dir:`autosummary` directive. :param items: A list produced by ``self.get_items``. :rtype: .. latex:clearpage:: """ table_spec, table, *other_nodes = super().get_table(items) assert isinstance(table_spec, addnodes.tabular_col_spec) assert isinstance(table, autosummary_table) if docutils.__version_info__ >= (0, 18): table.children[0]["classes"] += ["colwidths-given"] # type: ignore[index] column_type = getattr(self.env.config, "autosummary_col_type", r"\X") table_spec["spec"] = f'{column_type}{{1}}{{2}}{column_type}{{1}}{{2}}' return [table_spec, table, *other_nodes]
[docs]def get_documenter(app: Sphinx, obj: Any, parent: Any) -> Type[Documenter]: """ Returns an :class:`autodoc.Documenter` class suitable for documenting the given object. .. versionadded:: 1.3.0 :param app: The Sphinx application. :param obj: The object being documented. :param parent: The parent of the object (e.g. a module or a class). """ if inspect.ismodule(obj): # ModuleDocumenter.can_document_member always returns False return ModuleDocumenter # Construct a fake documenter for *parent* if parent is not None: parent_doc_cls = get_documenter(app, parent, None) else: parent_doc_cls = ModuleDocumenter if hasattr(parent, "__name__"): parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) else: parent_doc = parent_doc_cls(FakeDirective(), '') # Get the correct documenter class for *obj* classes = [ cls for cls in app.registry.documenters.values() if cls.can_document_member(obj, '', False, parent_doc) ] data_doc_classes = [ cls for cls in app.registry.documenters.values() if cls.can_document_member(obj, '', True, parent_doc) ] if classes: classes.sort(key=lambda cls: cls.priority) return classes[-1] elif data_doc_classes: data_doc_classes.sort(key=lambda cls: cls.priority) return data_doc_classes[-1] else: return DataDocumenter
[docs]class PatchedAutoSummModuleDocumenter(autodocsumm.AutoSummModuleDocumenter): """ Patched version of :class:`autodocsumm.AutoSummClassDocumenter` which works around a bug in Sphinx 3.4 and above where ``__all__`` is not respected. .. versionadded:: 2.13.0 """ # noqa: D400 def filter_members(self, members: ObjectMembers, want_all: bool) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. Members are skipped if: * they are private (except if given explicitly or the ``private-members`` option is set) * they are special methods (except if given explicitly or the ``special-members`` option is set) * they are undocumented (except if the ``undoc-members`` option is set) The user can override the skipping decision by connecting to the :event:`autodoc-skip-member` event. """ def is_filtered_inherited_member(name: str) -> bool: if inspect.isclass(self.object): for cls in self.object.__mro__: if cls.__name__ == self.options.inherited_members and cls != self.object: # given member is a member of specified *super class* return True elif name in cls.__dict__: return False elif name in self.get_attr(cls, "__annotations__", {}): return False return False ret = [] # search for members in source code too namespace = '.'.join(self.objpath) # will be empty for modules if self.analyzer: attr_docs = self.analyzer.find_attr_docs() else: attr_docs = {} doc: Optional[str] sphinx_gt_41 = sphinx.version_info > (4, 1) # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute isattr = (member is INSTANCEATTR or (namespace, membername) in attr_docs) doc = getdoc( member, self.get_attr, self.env.config.autodoc_inherit_docstrings, self.parent, self.object_name ) if not isinstance(doc, str): # Ignore non-string __doc__ doc = None # if the member __doc__ is the same as self's __doc__, it's just # inherited and therefore not the member's doc cls = self.get_attr(member, "__class__", None) if cls: cls_doc = self.get_attr(cls, "__doc__", None) if cls_doc == doc: doc = None if sphinx_gt_41: doc, metadata = separate_metadata(doc) # type: ignore[arg-type] else: metadata = extract_metadata(doc) has_doc = bool(doc) if "private" in metadata: # consider a member private if docstring has "private" metadata isprivate = True elif "public" in metadata: # consider a member public if docstring has "public" metadata isprivate = False else: isprivate = membername.startswith('_') keep = False if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object pass elif self.options.exclude_members and membername in self.options.exclude_members: # remove members given by exclude-members keep = False elif want_all and special_member_re.match(membername): # special __methods__ if self.options.special_members and membername in self.options.special_members: if membername == "__doc__": keep = False elif is_filtered_inherited_member(membername): keep = False else: keep = has_doc or self.options.undoc_members else: keep = False elif (namespace, membername) in attr_docs: if want_all and isprivate: if self.options.private_members is None: keep = False else: keep = membername in self.options.private_members else: # keep documented attributes keep = True isattr = True elif want_all and isprivate: if has_doc or self.options.undoc_members: if self.options.private_members is None: keep = False elif is_filtered_inherited_member(membername): keep = False else: keep = membername in self.options.private_members else: keep = False else: if self.options.members is ALL and is_filtered_inherited_member(membername): keep = False else: # ignore undocumented members if :undoc-members: is not given keep = has_doc or self.options.undoc_members # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: # pylint: disable=R8203 skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: msg = 'autodoc: failed to determine %r to be documented, the following exception was raised:\n%s' logger.warning(__(msg), member, exc, type="autodoc") keep = False if keep: ret.append((membername, member, isattr)) return ret def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: """ Return a tuple of ``(members_check_module, members)``, where ``members`` is a list of ``(membername, member)`` pairs of the members of ``self.object``. If ``want_all`` is :py:obj:`True`, return all members. Otherwise, only return those members given by ``self.options.members`` (which may also be none). """ # noqa: D400 if want_all: if self.__all__: memberlist = self.__all__ else: # for implicit module members, check __module__ to avoid # documenting imported objects return True, _get_module_members(self.object) else: memberlist = self.options.members or [] ret = [] for mname in memberlist: try: # pylint: disable=R8203 if sphinx.version_info >= (8, 0): # 3rd party from sphinx.ext.autodoc import ObjectMember ret.append(ObjectMember(mname, safe_getattr(self.object, mname))) else: ret.append((mname, safe_getattr(self.object, mname))) # type: ignore[arg-type] except AttributeError: # pylint: disable=dotted-import-in-loop) logger.warning( operator.mod( __("missing attribute mentioned in :members: or __all__: module %s, attribute %s"), (safe_getattr(self.object, "__name__", "???"), mname), ), type="autodoc" ) # pylint: enable=dotted-import-in-loop) return False, ret
[docs]class PatchedAutoSummClassDocumenter(autodocsumm.AutoSummClassDocumenter): """ Patched version of :class:`autodocsumm.AutoSummClassDocumenter` which doesn't show summary tables for aliased objects. .. versionadded:: 0.9.0 """ # noqa: D400
[docs] def add_content(self, *args, **kwargs) -> None: """ Add content from docstrings, attribute documentation and user. """ ClassDocumenter.add_content(self, *args, **kwargs) if not self.doc_as_attr: self.add_autosummary()
class PatchedAutoDocSummDirective(autodocsumm.AutoDocSummDirective): """ Patched ``AutoDocSummDirective`` which uses :py:obj:`None` for the members option rather than an empty string. .. attention:: This class is not part of the public API. """ def run(self) -> List[nodes.Node]: reporter = self.state.document.reporter if hasattr(reporter, "get_source_and_line"): _, lineno = reporter.get_source_and_line(self.lineno) else: _, lineno = (None, None) # look up target Documenter objtype = self.name[4:-4] # strip prefix (auto-) and suffix (-summ). doccls = self.env.app.registry.documenters[objtype] self.options["autosummary-force-inline"] = "True" self.options["autosummary"] = "True" if "no-members" not in self.options: self.options["members"] = None # process the options with the selected documenter's option_spec try: documenter_options = process_documenter_options(doccls, self.config, self.options) except (KeyError, ValueError, TypeError) as exc: # an option is either unknown or has a wrong type logger.error( "An option to %s is either unknown or has an invalid value: %s", self.name, exc, location=(self.env.docname, lineno), ) return [] # generate the output params = DocumenterBridge(self.env, reporter, documenter_options, lineno, self.state) documenter = doccls(params, self.arguments[0]) documenter.add_autosummary() node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(params.result, 0, node) return node.children def _patch_filter_members() -> None: # 3rd party from sphinx.ext.autodoc import ObjectMember orig_filter_members = Documenter.filter_members def _documenter_filter_members( self, members: List[ObjectMember], want_all: bool, ) -> List[Tuple[str, Any, bool]]: if members and isinstance(members[0], tuple): members = [ObjectMember(*m) for m in members] return orig_filter_members(self, members, want_all) Documenter.filter_members = _documenter_filter_members # type: ignore[method-assign, assignment]
[docs]@metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autosummary`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autosummary") app.setup_extension("autodocsumm") app.setup_extension("sphinx_toolbox.latex") app.add_directive("autosummary", PatchedAutosummary, override=True) app.add_directive("autoclasssumm", PatchedAutoDocSummDirective, override=True) app.add_directive("automodulesumm", PatchedAutoDocSummDirective, override=True) autodocsumm.AutosummaryDocumenter.add_autosummary = add_autosummary allow_subclass_add(app, PatchedAutoSummModuleDocumenter) allow_subclass_add(app, PatchedAutoSummClassDocumenter) app.add_config_value( "autodocsumm_member_order", default="alphabetical", rebuild=True, types=ENUM("alphabetic", "alphabetical", "bysource"), ) app.add_config_value( "autosummary_col_type", default=r"\X", rebuild="latex", types=[str], ) if sphinx.version_info >= (8, 0): # 3rd party from sphinx.ext.autodoc import ObjectMember # Restore deprecated and removed functionality to fix autodocsumm ObjectMember.__getitem__ = lambda self, idx: (self.__name__, self.object)[idx] # type: ignore[method-assign] _patch_filter_members() return {"parallel_read_safe": True}