Loading doc/.gitignore 0 → 100644 +4 −0 Original line number Diff line number Diff line # This project uses Markdown, any RST file is generated *.rst build/ doc/Makefile 0 → 100644 +56 −0 Original line number Diff line number Diff line SPHINXBUILD = sphinx-build SPHINXOPTS = -j auto -E SPHINXAPIDOC = sphinx-apidoc SPHINXAPIDOCOPTS = --separate BUILDDIR = build # Alternatives: html dirhtml singlehtml HTML_BUILDER = html # Values above this line may be modified from cmdline # ====== BUILD_SRC = Makefile $(wildcard *.py) SPHINX_SRC = index.md kilter.service.rst SPHINX_SRC += ../README.md APIDOC_SRC := $(wildcard ../kilter/service/*.py) APIDOC_SRC += $(wildcard templates/apidoc/*.rst_t) BUILDOUT = $(@D) .PHONY: default all default: .PHONY all default: html html: BUILDER=$(HTML_BUILDER) html: doctest $(BUILDDIR)/html/index.html .PHONY all: man man: BUILDER=man man: doctest $(BUILDDIR)/man/kilterkilterservice.1 .PHONY all: text text: BUILDER=text text: doctest $(BUILDDIR)/text/index.txt .PHONY: doctest doctest: BUILDER=doctest doctest: $(BUILDDIR)/doctest/output.txt $(APIDOC_SRC) .PHONY: clean clean: -rm -rf $(BUILDDIR) -rm kilter.* kilter.service.rst: $(BUILD_SRC) $(APIDOC_SRC) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) --implicit-namespaces -MTft templates/apidoc -o . ../kilter @rm kilter.rst @touch $@ $(BUILDDIR)/%: $(BUILD_SRC) $(SPHINX_SRC) | $(BUILDDIR) $(SPHINXBUILD) -ab $(BUILDER) $(SPHINXOPTS) . $(BUILDOUT) $(BUILDDIR): mkdir -p $@ doc/conf.py 0 → 100644 +58 −0 Original line number Diff line number Diff line from __future__ import annotations import sys from pathlib import Path project = "Kilter (kilter.service)" highlight_language = "python3" add_module_names = False html_theme = "sphinx_rtd_theme" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "myst_parser", "docstring", ] myst_enable_extensions = [ "substitution", ] doc_dir = Path(__file__).parent.absolute() sys.path.insert(0, str(doc_dir)) sys.path.insert(0, str(doc_dir.parent)) autoclass_content = "class" autodoc_class_signature = "mixed" autodoc_member_order = "bysource" autodoc_typehints = "both" autodoc_typehints_description_target = "documented" autodoc_typehints_format = "short" autodoc_inherit_docstring = True doctest_test_doctest_blocks = "default" myst_heading_anchors = 3 myst_substitutions = { "libmilter": "*__libmilter__*", } myst_enable_extensions += [ "smartquotes", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "kilter.protocol": ("http://dom.doc.kodo.org.uk/kilter.protocol", None), "anyio": ("https://anyio.readthedocs.io/en/stable", None), } autodoc_type_aliases = { "EventMessage": "kilter.service.session.EventMessage", "ResponseMessage": "kilter.service.session.ResponseMessage", "EditMessage": "kilter.service.session.EditMessage", } doc/docstring.py 0 → 100644 +210 −0 Original line number Diff line number Diff line """ A Sphinx extension to make cross-linking work without polluting docstrings with RST markup Docstring text surrounded by single backticks with optional calling parentheses is examined to see if it can be resolved into a Python object, which is then injected as a ":py:xxx:`<object>`" style link. """ from __future__ import annotations import builtins import re from collections.abc import Callable from importlib import import_module from inspect import get_annotations from types import FunctionType from types import ModuleType from typing import Literal as L from typing import Union from sphinx.application import Sphinx from sphinx.util import logging ObjType = Union[ L["module"], L["class"], L["exception"], L["function"], L["method"], L["attribute"], L["property"], ] class UnknownObject(ValueError): """ An error for unknown values of "what" or unusable "obj" passed to `add_roles` """ def setup(app: Sphinx) -> None: app.connect("autodoc-process-docstring", add_roles) def add_roles( app: Sphinx, what: ObjType, name: str, obj: object, options: object, lines: list[str], ) -> None: """ Add Sphinx roles to strings delimited with "`" in docstrings Polluting docstrings with RestructuredText markup is forbidden, so this plugin marks-up python objects in backticks for cross linking. """ replacer = get_replacer(what, obj, name) regex = re.compile(r"(?<![:])`(?P<name>[a-z0-9_.]+)(\(\))?`", re.I) lines[:] = (regex.sub(replacer, line) for line in lines) def get_replacer( what: ObjType, doc_obj: object, doc_obj_name: str, ) -> Callable[[re.Match[str]], str]: try: module, cls = get_context(what, doc_obj) except UnknownObject: return lambda m: m.group(0) def get_type(match: re.Match[str]) -> str: """ Given a match for a dot-name, return the RST type """ name = match.group("name") try: obj, parent, name = dot_import(module, cls, name) except AttributeError: location = None if isinstance(doc_obj, FunctionType): co = doc_obj.__code__ location = (co.co_filename, co.co_firstlineno) logging.getLogger(__name__).warning( f"ignoring {match.group(0)} in docstring of {doc_obj_name}", type="ref.ref", location=location, ) return match.group(0) if isinstance(obj, ModuleType): role = ":py:mod:" elif isinstance(obj, type): role = ":py:exc:" if issubclass(obj, BaseException) else ":py:class:" elif callable(obj): role = ":py:meth:" if isinstance(parent, type) else ":py:func:" elif isinstance(parent, ModuleType): role = ":py:const:" if name.isupper() else ":py:data:" elif isinstance(parent, type): role = ":py:attr:" else: role = ":py:obj:" if isinstance(parent, ModuleType): name = f"{name.removeprefix(parent.__name__+'.')} <{name}>" elif isinstance(parent, type): name = f"{name.removeprefix(parent.__module__+'.')} <{name}>" return f"{role}`{name}`" return get_type def get_context(what: ObjType, obj: object) -> tuple[ModuleType, type|None]: """ Given an object and its type, return the module it's in and a class if appropriate These values form the starting points for searching for names. """ match what: case "module": assert isinstance(obj, ModuleType) return obj, None case "method": assert isinstance(obj, FunctionType) return get_method_context(obj) case "property": assert isinstance(obj, property) func = \ obj.fget if isinstance(obj.fget, FunctionType) else \ obj.fset if isinstance(obj.fset, FunctionType) else \ None if func is None: raise UnknownObject( "could not get function from property; cannot determine a module", ) return get_method_context(func) case "class" | "exception": assert isinstance(obj, type), f"{what} {obj!r} is not a type?!" return import_module(obj.__module__), obj case "function": assert hasattr(obj, "__module__"), f"{what} {obj!r} has no attribute '__module__'" return import_module(obj.__module__), None raise UnknownObject(f"unknown value for 'what': {what}") def get_method_context(method: FunctionType) -> tuple[ModuleType, type|None]: """ Given a method, return it's module and best attempt at a class """ mod = import_module(method.__module__) clsname, has_clsname, _ = method.__qualname__.rpartition(".") if not has_clsname: return mod, None return mod, getattr(mod, clsname, None) def dot_import(module: ModuleType, cls: type|None, name: str) -> tuple[object, object, str]: """ Given a dot-separated name, return an object, its parent, and an absolute name for it The search is started from the context returned by `get_context()`. """ labels = list(name.split(".")) obj, parent, name = dot_import_first(module, cls, labels.pop(0)) for label in labels: parent = obj match obj: case ModuleType(): obj = dot_import_from(obj, label) case type(): try: obj = getattr(obj, label) except AttributeError: assert isinstance(obj, type) # come on mypy… annotations = get_annotations(obj) if label not in annotations: raise obj = annotations[label] case _: obj = getattr(obj, label) return obj, parent, ".".join([name] + labels) def dot_import_first(module: ModuleType, cls: type|None, name: str) -> tuple[object, object, str]: """ Given a name, return an object, its parent, and its absolute dot-separated name The name is search first from builtins; then top-level packages and modules; then submodules of the context module; then attributes of the context modules; then attributes of the context class, or as a special case the context class itself if the name is "self". """ try: return getattr(builtins, name), None, name except AttributeError: pass try: return import_module(name), None, name except ModuleNotFoundError: pass try: obj = dot_import_from(module, name) if hasattr(obj, "__module__"): module = import_module(obj.__module__) return obj, module, f"{module.__name__}.{name}" except AttributeError: if cls is None: raise return ( (cls, module, f"{module.__name__}.{cls.__name__}") if name == "self" else \ (getattr(cls, name), cls, f"{module.__name__}.{cls.__name__}.{name}") ) def dot_import_from(module: ModuleType, name: str) -> object: """ Given a module and name, return a submodule or module attribute of that name """ try: return import_module("." + name, module.__name__) except ModuleNotFoundError: return getattr(module, name) doc/index.md 0 → 100644 +14 −0 Original line number Diff line number Diff line Kilter Service (kilter.service) =============================== ```{include} ../README.md :relative-docs: doc/ :start-line: 3 ``` ```{toctree} :maxdepth: 4 :caption: Sections kilter.service ``` Loading
doc/.gitignore 0 → 100644 +4 −0 Original line number Diff line number Diff line # This project uses Markdown, any RST file is generated *.rst build/
doc/Makefile 0 → 100644 +56 −0 Original line number Diff line number Diff line SPHINXBUILD = sphinx-build SPHINXOPTS = -j auto -E SPHINXAPIDOC = sphinx-apidoc SPHINXAPIDOCOPTS = --separate BUILDDIR = build # Alternatives: html dirhtml singlehtml HTML_BUILDER = html # Values above this line may be modified from cmdline # ====== BUILD_SRC = Makefile $(wildcard *.py) SPHINX_SRC = index.md kilter.service.rst SPHINX_SRC += ../README.md APIDOC_SRC := $(wildcard ../kilter/service/*.py) APIDOC_SRC += $(wildcard templates/apidoc/*.rst_t) BUILDOUT = $(@D) .PHONY: default all default: .PHONY all default: html html: BUILDER=$(HTML_BUILDER) html: doctest $(BUILDDIR)/html/index.html .PHONY all: man man: BUILDER=man man: doctest $(BUILDDIR)/man/kilterkilterservice.1 .PHONY all: text text: BUILDER=text text: doctest $(BUILDDIR)/text/index.txt .PHONY: doctest doctest: BUILDER=doctest doctest: $(BUILDDIR)/doctest/output.txt $(APIDOC_SRC) .PHONY: clean clean: -rm -rf $(BUILDDIR) -rm kilter.* kilter.service.rst: $(BUILD_SRC) $(APIDOC_SRC) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) --implicit-namespaces -MTft templates/apidoc -o . ../kilter @rm kilter.rst @touch $@ $(BUILDDIR)/%: $(BUILD_SRC) $(SPHINX_SRC) | $(BUILDDIR) $(SPHINXBUILD) -ab $(BUILDER) $(SPHINXOPTS) . $(BUILDOUT) $(BUILDDIR): mkdir -p $@
doc/conf.py 0 → 100644 +58 −0 Original line number Diff line number Diff line from __future__ import annotations import sys from pathlib import Path project = "Kilter (kilter.service)" highlight_language = "python3" add_module_names = False html_theme = "sphinx_rtd_theme" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "myst_parser", "docstring", ] myst_enable_extensions = [ "substitution", ] doc_dir = Path(__file__).parent.absolute() sys.path.insert(0, str(doc_dir)) sys.path.insert(0, str(doc_dir.parent)) autoclass_content = "class" autodoc_class_signature = "mixed" autodoc_member_order = "bysource" autodoc_typehints = "both" autodoc_typehints_description_target = "documented" autodoc_typehints_format = "short" autodoc_inherit_docstring = True doctest_test_doctest_blocks = "default" myst_heading_anchors = 3 myst_substitutions = { "libmilter": "*__libmilter__*", } myst_enable_extensions += [ "smartquotes", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "kilter.protocol": ("http://dom.doc.kodo.org.uk/kilter.protocol", None), "anyio": ("https://anyio.readthedocs.io/en/stable", None), } autodoc_type_aliases = { "EventMessage": "kilter.service.session.EventMessage", "ResponseMessage": "kilter.service.session.ResponseMessage", "EditMessage": "kilter.service.session.EditMessage", }
doc/docstring.py 0 → 100644 +210 −0 Original line number Diff line number Diff line """ A Sphinx extension to make cross-linking work without polluting docstrings with RST markup Docstring text surrounded by single backticks with optional calling parentheses is examined to see if it can be resolved into a Python object, which is then injected as a ":py:xxx:`<object>`" style link. """ from __future__ import annotations import builtins import re from collections.abc import Callable from importlib import import_module from inspect import get_annotations from types import FunctionType from types import ModuleType from typing import Literal as L from typing import Union from sphinx.application import Sphinx from sphinx.util import logging ObjType = Union[ L["module"], L["class"], L["exception"], L["function"], L["method"], L["attribute"], L["property"], ] class UnknownObject(ValueError): """ An error for unknown values of "what" or unusable "obj" passed to `add_roles` """ def setup(app: Sphinx) -> None: app.connect("autodoc-process-docstring", add_roles) def add_roles( app: Sphinx, what: ObjType, name: str, obj: object, options: object, lines: list[str], ) -> None: """ Add Sphinx roles to strings delimited with "`" in docstrings Polluting docstrings with RestructuredText markup is forbidden, so this plugin marks-up python objects in backticks for cross linking. """ replacer = get_replacer(what, obj, name) regex = re.compile(r"(?<![:])`(?P<name>[a-z0-9_.]+)(\(\))?`", re.I) lines[:] = (regex.sub(replacer, line) for line in lines) def get_replacer( what: ObjType, doc_obj: object, doc_obj_name: str, ) -> Callable[[re.Match[str]], str]: try: module, cls = get_context(what, doc_obj) except UnknownObject: return lambda m: m.group(0) def get_type(match: re.Match[str]) -> str: """ Given a match for a dot-name, return the RST type """ name = match.group("name") try: obj, parent, name = dot_import(module, cls, name) except AttributeError: location = None if isinstance(doc_obj, FunctionType): co = doc_obj.__code__ location = (co.co_filename, co.co_firstlineno) logging.getLogger(__name__).warning( f"ignoring {match.group(0)} in docstring of {doc_obj_name}", type="ref.ref", location=location, ) return match.group(0) if isinstance(obj, ModuleType): role = ":py:mod:" elif isinstance(obj, type): role = ":py:exc:" if issubclass(obj, BaseException) else ":py:class:" elif callable(obj): role = ":py:meth:" if isinstance(parent, type) else ":py:func:" elif isinstance(parent, ModuleType): role = ":py:const:" if name.isupper() else ":py:data:" elif isinstance(parent, type): role = ":py:attr:" else: role = ":py:obj:" if isinstance(parent, ModuleType): name = f"{name.removeprefix(parent.__name__+'.')} <{name}>" elif isinstance(parent, type): name = f"{name.removeprefix(parent.__module__+'.')} <{name}>" return f"{role}`{name}`" return get_type def get_context(what: ObjType, obj: object) -> tuple[ModuleType, type|None]: """ Given an object and its type, return the module it's in and a class if appropriate These values form the starting points for searching for names. """ match what: case "module": assert isinstance(obj, ModuleType) return obj, None case "method": assert isinstance(obj, FunctionType) return get_method_context(obj) case "property": assert isinstance(obj, property) func = \ obj.fget if isinstance(obj.fget, FunctionType) else \ obj.fset if isinstance(obj.fset, FunctionType) else \ None if func is None: raise UnknownObject( "could not get function from property; cannot determine a module", ) return get_method_context(func) case "class" | "exception": assert isinstance(obj, type), f"{what} {obj!r} is not a type?!" return import_module(obj.__module__), obj case "function": assert hasattr(obj, "__module__"), f"{what} {obj!r} has no attribute '__module__'" return import_module(obj.__module__), None raise UnknownObject(f"unknown value for 'what': {what}") def get_method_context(method: FunctionType) -> tuple[ModuleType, type|None]: """ Given a method, return it's module and best attempt at a class """ mod = import_module(method.__module__) clsname, has_clsname, _ = method.__qualname__.rpartition(".") if not has_clsname: return mod, None return mod, getattr(mod, clsname, None) def dot_import(module: ModuleType, cls: type|None, name: str) -> tuple[object, object, str]: """ Given a dot-separated name, return an object, its parent, and an absolute name for it The search is started from the context returned by `get_context()`. """ labels = list(name.split(".")) obj, parent, name = dot_import_first(module, cls, labels.pop(0)) for label in labels: parent = obj match obj: case ModuleType(): obj = dot_import_from(obj, label) case type(): try: obj = getattr(obj, label) except AttributeError: assert isinstance(obj, type) # come on mypy… annotations = get_annotations(obj) if label not in annotations: raise obj = annotations[label] case _: obj = getattr(obj, label) return obj, parent, ".".join([name] + labels) def dot_import_first(module: ModuleType, cls: type|None, name: str) -> tuple[object, object, str]: """ Given a name, return an object, its parent, and its absolute dot-separated name The name is search first from builtins; then top-level packages and modules; then submodules of the context module; then attributes of the context modules; then attributes of the context class, or as a special case the context class itself if the name is "self". """ try: return getattr(builtins, name), None, name except AttributeError: pass try: return import_module(name), None, name except ModuleNotFoundError: pass try: obj = dot_import_from(module, name) if hasattr(obj, "__module__"): module = import_module(obj.__module__) return obj, module, f"{module.__name__}.{name}" except AttributeError: if cls is None: raise return ( (cls, module, f"{module.__name__}.{cls.__name__}") if name == "self" else \ (getattr(cls, name), cls, f"{module.__name__}.{cls.__name__}.{name}") ) def dot_import_from(module: ModuleType, name: str) -> object: """ Given a module and name, return a submodule or module attribute of that name """ try: return import_module("." + name, module.__name__) except ModuleNotFoundError: return getattr(module, name)
doc/index.md 0 → 100644 +14 −0 Original line number Diff line number Diff line Kilter Service (kilter.service) =============================== ```{include} ../README.md :relative-docs: doc/ :start-line: 3 ``` ```{toctree} :maxdepth: 4 :caption: Sections kilter.service ```