i3pystatus/docs/module_docs.py

195 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pkgutil
import importlib
import sphinx.application
from docutils.parsers.rst import Directive
from docutils.nodes import paragraph
from docutils.statemachine import StringList
import i3pystatus.core.settings
import i3pystatus.core.modules
from i3pystatus.core.imputil import ClassFinder
from i3pystatus.core.color import ColorRangeModule
IGNORE_MODULES = ("__main__", "core", "tools")
def is_module(obj):
return (isinstance(obj, type)
and issubclass(obj, i3pystatus.core.settings.SettingsBase)
and not obj.__module__.startswith("i3pystatus.core."))
def fail_on_missing_dependency_hints(obj, lines):
# We can automatically check in some cases if we forgot something
if issubclass(obj, ColorRangeModule):
if all("colour" not in line for line in lines):
raise ValueError(">>> Module <{}> uses ColorRangeModule and should document it <<<\n"
"> Requires the PyPI package ``colour``".format(obj.__name__))
def check_settings_consistency(obj, settings):
errs = []
for setting in settings:
if not setting.required and setting.default is setting.sentinel:
errs.append("<" + setting.name + ">")
if errs:
raise ValueError(">>> Module <{}> has non-required setting(s) {} with no default! <<<\n"
.format(obj.__name__, ", ".join(errs)))
def process_docstring(app, what, name, obj, options, lines):
class Setting:
doc = ""
required = False
default = sentinel = object()
empty = object()
def __init__(self, cls, setting):
if isinstance(setting, tuple):
self.name = setting[0]
self.doc = setting[1]
else:
self.name = setting
if self.name in cls.required:
self.required = True
elif hasattr(cls, self.name):
default = getattr(cls, self.name)
if isinstance(default, str) and not len(default)\
or default is None:
default = self.empty
self.default = default
def __str__(self):
attrs = []
if self.required:
attrs.append("required")
if self.default not in [self.sentinel, self.empty]:
attrs.append("default: ``{default}``".format(default=self.default))
if self.default is self.empty:
attrs.append("default: *empty*")
formatted = "* **{name}** {attrsf} {doc}".format(
name=self.name,
doc=" " + self.doc if self.doc else "",
attrsf=" ({attrs})".format(attrs=", ".join(attrs)) if attrs else "")
return formatted
if is_module(obj) and obj.settings:
fail_on_missing_dependency_hints(obj, lines)
if issubclass(obj, i3pystatus.core.modules.Module):
mod = obj.__module__
if mod.startswith("i3pystatus."):
mod = mod[len("i3pystatus."):]
lines[0:0] = [
".. raw:: html",
"",
" <div class='modheader'>" +
"Module name: <code class='modname descclassname'>" + mod + "</code> " +
"(class <code class='descclassname'>" + name + "</code>)" +
"</div>",
"",
]
else:
lines[0:0] = [
".. raw:: html",
"",
" <div class='modheader'>class <code class='descclassname'>" + name + "</code></div>",
"",
]
lines.append(".. rubric:: Settings")
lines.append("")
settings = [Setting(obj, setting) for setting in obj.settings]
lines += map(str, settings)
check_settings_consistency(obj, settings)
lines.append("")
def process_signature(app, what, name, obj, options, signature, return_annotation):
if is_module(obj):
return ("", return_annotation)
def get_modules(path, package):
modules = []
for finder, modname, is_package in pkgutil.iter_modules(path):
if modname not in IGNORE_MODULES:
modules.append(get_module(finder, modname, package))
return modules
def get_module(finder, modname, package):
fullname = "{package}.{modname}".format(package=package, modname=modname)
return (modname, finder.find_loader(fullname)[0].load_module(fullname))
def get_all(module_path, modname, basecls):
mods = []
finder = ClassFinder(basecls)
for name, module in get_modules(module_path, modname):
classes = finder.get_matching_classes(module)
found = []
for cls in classes:
if cls.__name__ not in found:
found.append(cls.__name__)
mods.append((module.__name__, cls.__name__))
return sorted(mods, key=lambda module: module[0])
def generate_automodules(path, name, basecls):
modules = get_all(path, name, basecls)
contents = []
for mod in modules:
contents.append("* :py:mod:`~{}`".format(mod[0]))
contents.append("")
for mod in modules:
contents.append(".. _{}:\n".format(mod[0].split(".")[-1]))
contents.append(".. automodule:: {}".format(mod[0]))
contents.append(" :members: {}\n".format(mod[1]))
return contents
class AutogenDirective(Directive):
required_arguments = 2
has_content = True
def run(self):
# Raise an error if the directive does not have contents.
self.assert_has_content()
modname = self.arguments[0]
modpath = importlib.import_module(modname).__path__
basecls = getattr(i3pystatus.core.modules, self.arguments[1])
contents = []
for e in self.content:
contents.append(e)
contents.append("")
contents.extend(generate_automodules(modpath, modname, basecls))
node = paragraph()
self.state.nested_parse(StringList(contents), 0, node)
return [node]
def setup(app: sphinx.application.Sphinx):
app.add_directive("autogen", AutogenDirective)
app.connect("autodoc-process-docstring", process_docstring)
app.connect("autodoc-process-signature", process_signature)