import inspect
from more_itertools import always_iterable
from . import utils
from .errors import ConfigError
import re
SECTIONS = {}
class Section:
"""
Format a specific section in a class TOC, e.g. "Public Methods".
The purpose of this class is to make it easy to customize the sections that
make up the class TOC. For example, you might want an "Event Handler"
section that includes any method that starts with "on\\_". Or you might
want to format the links in a table with multiple columns, to save more
space.
These kinds of things can be accomplished by subclassing `Section` and
overwriting the relevant methods. Almost every method is meant to be
overridden by subclasses, but most subclasses will only need to override
`key`, `title`, and `predicate`. `key` and `title` have no default value,
and must be overridden in each subclass. `predicate` determines which
attributes are included in the section, which is the primary purpose of
most custom sections.
"""
key = None
"""
A string that can be used to refer to this section, e.g. in ``conf.py``
settings such as :confval:`autoclasstoc_sections` and
:rst:dir:`autoclasstoc` options such as ``:sections:`` and
``:exclude-sections:``.
"""
title = None
"""
The text that will be used to label this section.
It's conventional to end the title with a trailing colon.
"""
include_inherited = True
"""
Whether or not to include inherited attributes in this section.
"""
exclude_pattern = None
"""
A regular expression (or list of regular expressions) matching attribute
names that should be excluded from this section.
"""
exclude_section = None
"""
A section class (or list of section classes) whose members should be
excluded from this section.
"""
[docs] def __init__(self, state, cls):
"""
Create a section for a specific class.
Arguments:
state (docutils.parsers.rst.states.RSTState): The state object
associated with the :rst:dir:`autoclasstoc` directive. This
can be used to evaluate restructured text markup using
`nodes_from_rst()`.
cls (type): The class to make the TOC section for.
"""
self.state = state
self.cls = cls
[docs] def __init_subclass__(cls):
"""
Keep track of any `Section` subclasses that are defined.
"""
SECTIONS[cls.key] = cls
[docs] def check(self):
"""
Raise `ConfigError` if the section has not been configured correctly,
e.g. if it doesn't have a title specified.
"""
if not self.key:
raise ConfigError(
f"no key specified for {self.__class__.__name__!r}")
if not self.title:
raise ConfigError(
f"no title specified for {self.__class__.__name__!r}")
[docs] def predicate(self, name, attr, meta):
"""
Return true if the given attribute should be included in this section.
Arguments:
name (str): The name of the attribute. In most cases, this is
identical to :attr:`attr.__name__`.
attr (object): The attribute object itself.
meta (dict): Any `:meta:`__ fields present in the attribute's
docstring, as parsed by
:func:`sphinx.util.docstrings.separate_metadata()`.
__ https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists
See Also:
`is_method`
`is_data_attr`
`is_public`
`is_private`
`is_special`
"""
if does_match(name, self.exclude_pattern):
return False
for section_cls in always_iterable(self.exclude_section):
section = section_cls(self.state, self.cls)
if section.predicate(name, attr, meta):
return False
return True
[docs] def _make_container(self):
"""
Create the container node that will contain the entire section.
This method is meant to be overridden in subclasses. The primary
purpose of the container node is to belong to a CSS class that can then
be used to identify HTML elements associated with
:rst:dir:`autoclasstoc`.
"""
return utils.make_container()
[docs] def _make_rubric(self):
"""
Create the section header node.
This method is meant to be overridden in subclasses.
"""
return utils.make_rubric(self.title)
[docs] def _make_links(self, attrs, cls):
"""
Make a link to the full documentation for each attribute.
This method is meant to be overridden in subclasses. The default
implementation creates the links using an :rst:dir:`autosummary`
directive.
Arguments:
attrs (dict): A dictionary of attributes, in the same format as
``__dict__``.
cls (type): The class that the attributes belong to.
"""
return utils.make_links(self.state, attrs, cls)
[docs] def _make_inherited_details(self, parent):
"""
Make a collapsible node to contain links to inherited attributes.
This method is meant to be overridden in subclasses. The default
implementation returns a `details` node, which is rendered in HTML as a
``<details>`` element.
"""
return utils.make_inherited_details(self.state, parent)
[docs] def _filter_attrs(self, attrs):
"""
Return only those attributes that match the predicate.
Arguments:
attrs (dict): A dictionary of attributes, in the same format as
``__dict__``. Attributes have only an annotation, but no
value, will be present in this dictionary, but will have the
special value `utils.ANNOTATED_ATTR`.
Return:
A dictionary in the same format as *attrs*.
"""
return utils.filter_attrs(attrs, self.predicate)
[docs] def _find_attrs(self):
"""
Return all attributes associated with this class.
These attributes will subsequently be filtered to remove any that
aren't relevant to this section, so there is no need to do any
filtering here. The return value should be a name-to-attribute
dictionary in the same format as :attr:`__dict__`.
"""
return utils.find_attrs(self.cls)
[docs] def _find_inherited_attrs(self):
"""
Find attributes that this class has inherited from other classes.
These attributes will subsequently be filtered to remove any that
aren't relevant to this section, so there is no need to do any
filtering here. The return value should be a dictionary mapping parent
class types to :attr:`__dict__` style dictionaries.
"""
return utils.find_inherited_attrs(self.cls)
class PublicMethods(Section):
"""
Include a "Public Methods" section in the class TOC.
Note that dunder methods are included in this section, because they usually
correspond to syntax that is part of the class's public interface (e.g. the
constructor and various operators). If you want to exclude dunder methods,
use `PublicMethodsWithoutDunders`.
"""
key = 'public-methods'
title = "Public Methods:"
[docs] def predicate(self, name, attr, meta):
return (
super().predicate(name, attr, meta) and
is_method(name, attr) and
(is_public(name) or is_special(name))
)
class PublicMethodsWithoutDunders(PublicMethods):
"""
Include a "Public Methods" section in the class TOC.
"""
key = 'public-methods-without-dunders'
exclude_pattern = '__'
class PrivateMethods(Section):
"""
Include a "Private Methods" section in the class TOC.
"""
key = 'private-methods'
title = "Private Methods:"
[docs] def predicate(self, name, attr, meta):
return (
super().predicate(name, attr, meta) and
is_method(name, attr) and
is_private(name)
)
class PublicDataAttrs(Section):
"""
Include a "Public Data Attributes" section in the class TOC.
Note that only data attributes defined at the class level will be included
in the TOC. Data attributes defined in :meth:`__init__` (for example) will
not be found.
"""
key = 'public-attrs'
title = "Public Data Attributes:"
[docs] def predicate(self, name, attr, meta):
return (
super().predicate(name, attr, meta) and
is_data_attr(name, attr) and
is_public(name)
)
class PrivateDataAttrs(Section):
"""
Include a "Private Data Attributes" section in the class TOC.
Note that only data attributes defined at the class level will be included
in the TOC. Data attributes defined in :meth:`__init__` (for example) will
not be found.
"""
key = 'private-attrs'
title = "Private Data Attributes:"
[docs] def predicate(self, name, attr, meta):
return (
super().predicate(name, attr, meta) and
is_data_attr(name, attr) and
is_private(name)
)
class InnerClasses(Section):
"""
Include an "Inner Classes" section in the class TOC.
"""
key = 'inner-classes'
title = "Inner Classes:"
[docs] def predicate(self, name, attr, meta):
return (
super().predicate(name, attr, meta) and
inspect.isclass(attr)
)
def is_method(name, attr):
"""
Return true if the given attribute is a method or property.
"""
return any([
inspect.isfunction(attr),
inspect.ismethoddescriptor(attr),
])
def is_data_attr(name, attr):
"""
Return true if the given attribute is a data attribute, e.g. not a method
or an inner class. Many data attributes are properties.
"""
return all([
not inspect.isclass(attr),
not inspect.isfunction(attr),
not inspect.ismethoddescriptor(attr),
])
def is_public(name):
"""
Return true if the given name is public.
Specifically, a name is public if it either doesn't start with an
underscore.
"""
return not name.startswith('_')
def is_private(name):
"""
Return true if the given name is private.
A name is private if it starts with an underscore, but does not start and
end with two underscores (i.e. not a special method).
"""
return not is_public(name) and not is_special(name)
def is_special(name):
"""
Return True if the name starts and ends with a double-underscore.
Such names typically have special meaning to Python, e.g. :meth:`__init__`.
"""
return name.startswith('__') and name.endswith('__')
def does_match(name, pattern, **kwargs):
"""
Return true if the name matches the given pattern.
Under the hood, `re.match` is used to find the pattern. This means that
the match must start at the beginning of the name. If you want to match an
internal pattern, the pattern must start with ``.*``.
"""
return pattern and any(
re.match(p, name, **kwargs)
for p in always_iterable(pattern)
)