Source code for property_manager.sphinx

# Useful property variants for Python programming.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: March 2, 2020
# URL: https://property-manager.readthedocs.io

"""
Integration with the Sphinx_ documentation generator.

The :mod:`property_manager.sphinx` module uses the `Sphinx extension API`_ to
customize the process of generating Sphinx based Python documentation. It
modifies the documentation of :class:`.PropertyManager` subclasses to include
an overview of superclasses, properties, public methods and special methods. It
also includes hints about required properties and how the values of properties
can be set by passing keyword arguments to the class initializer.

For a simple example check out the documentation of the :class:`TypeInspector`
class. Yes, that means this module is being used to document itself :-).

The entry point to this module is the :func:`setup()` function.

.. _Sphinx extension API: http://sphinx-doc.org/extdev/appapi.html
"""

# Standard library modules.
import types

# Modules included in our package.
from property_manager import PropertyManager, custom_property, lazy_property, required_property
from humanfriendly.tables import format_rst_table
from humanfriendly.text import compact, concatenate, format

# Public identifiers that require documentation.
__all__ = (
    'setup',
    'append_property_docs',
    'TypeInspector',
)


[docs]def setup(app): """ Make it possible to use :mod:`property_manager.sphinx` as a Sphinx extension. :param app: The Sphinx application object. To enable the use of this module you add the name of the module to the ``extensions`` option in your ``docs/conf.py`` script: .. code-block:: python extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'property_manager.sphinx', ] When Sphinx sees the :mod:`property_manager.sphinx` name it will import this module and call the :func:`setup()` function which will connect the :func:`append_property_docs()` function to ``autodoc-process-docstring`` events. """ app.connect('autodoc-process-docstring', append_property_docs)
[docs]def append_property_docs(app, what, name, obj, options, lines): """ Render an overview with properties and methods of :class:`.PropertyManager` subclasses. This function implements a callback for ``autodoc-process-docstring`` that generates and appends an overview of member details to the docstrings of :class:`.PropertyManager` subclasses. The parameters expected by this function are those defined for Sphinx event callback functions (i.e. I'm not going to document them here :-). """ if is_suitable_type(obj): paragraphs = [] details = TypeInspector(type=obj) paragraphs.append(format("Here's an overview of the :class:`%s` class:", obj.__name__)) # Whitespace in labels is replaced with non breaking spaces to disable wrapping of the label text. data = [(format("%s:", label.replace(' ', u'\u00A0')), text) for label, text in details.overview if text] paragraphs.append(format_rst_table(data)) # Append any hints after the overview. hints = (details.required_hint, details.initializer_hint) if any(hints): paragraphs.append(' '.join(h for h in hints if h)) # Insert padding between the regular docstring and generated content. if lines: lines.append('') lines.extend('\n\n'.join(paragraphs).splitlines())
def is_suitable_type(obj): try: return issubclass(obj, PropertyManager) except Exception: return False
[docs]class TypeInspector(PropertyManager): """Introspection of :class:`.PropertyManager` subclasses."""
[docs] @lazy_property def custom_properties(self): """A list of tuples with the names and values of custom properties.""" return [(n, v) for n, v in self.properties if isinstance(v, custom_property)]
[docs] @lazy_property def initializer_hint(self): """A hint that properties can be set using keyword arguments to the initializer (a string or :data:`None`).""" names = sorted( name for name, value in self.custom_properties if value.key or value.required or value.writable ) if names: return compact( """ You can set the {values} of the {names} {properties} by passing {arguments} to the class initializer. """, names=self.format_properties(names), values=("value" if len(names) == 1 else "values"), properties=("property" if len(names) == 1 else "properties"), arguments=("a keyword argument" if len(names) == 1 else "keyword arguments"), )
[docs] @lazy_property def members(self): """An iterable of tuples with the names and values of the non-inherited members of :class:`type`.""" return list(self.type.__dict__.items())
[docs] @lazy_property def methods(self): """An iterable of method names of :class:`type`.""" return sorted(n for n, v in self.members if isinstance(v, types.FunctionType))
[docs] @lazy_property def overview(self): """Render an overview with related members grouped together.""" return ( ("Superclass" if len(self.type.__bases__) == 1 else "Superclasses", concatenate(format(":class:`~%s.%s`", b.__module__, b.__name__) for b in self.type.__bases__)), ("Special methods", self.format_methods(self.special_methods)), ("Public methods", self.format_methods(self.public_methods)), ("Properties", self.format_properties(n for n, v in self.properties)), )
[docs] @lazy_property def properties(self): """An iterable of tuples with property names (strings) and values (:class:`property` objects).""" return [(n, v) for n, v in self.members if isinstance(v, property)]
[docs] @lazy_property def public_methods(self): """An iterable of strings with the names of public methods (that don't start with an underscore).""" return sorted(n for n in self.methods if not n.startswith('_'))
[docs] @lazy_property def required_hint(self): """A hint about required properties (a string or :data:`None`).""" names = sorted(name for name, value in self.custom_properties if value.required) if names: return compact( """ When you initialize a :class:`{type}` object you are required to provide {values} for the {required} {properties}. """, type=self.type.__name__, required=self.format_properties(names), values=("a value" if len(names) == 1 else "values"), properties=("property" if len(names) == 1 else "properties"), )
[docs] @lazy_property def special_methods(self): """An iterable of strings with the names of special methods (surrounded in double underscores).""" methods = sorted(name for name in self.methods if name.startswith('__') and name.endswith('__')) if '__init__' in methods: methods.remove('__init__') methods.insert(0, '__init__') return methods
[docs] @required_property def type(self): """A subclass of :class:`.PropertyManager`."""
[docs] def format_methods(self, names): """Format a list of method names as reStructuredText.""" return concatenate(format(":func:`%s()`", n) for n in sorted(names))
[docs] def format_properties(self, names): """Format a list of property names as reStructuredText.""" return concatenate(format(":attr:`%s`", n) for n in sorted(names))