import inspect import os import re import sys import textwrap as tw from contextlib import contextmanager from sphinx.application import Sphinx base_path = os.path.abspath( os.path.join(os.path.dirname(os.path.relpath(__file__)), '..', '..', '..') ) sys.path.insert(0, base_path) from platypush.utils import get_plugin_name_by_class # noqa from platypush.utils.mock import mock # noqa from platypush.utils.reflection import IntegrationMetadata, import_file # noqa class IntegrationEnricher: @staticmethod def add_events(source: list[str], manifest: IntegrationMetadata, idx: int) -> int: if not manifest.events: return idx source.insert( idx, 'Triggered events\n----------------\n\n' + '\n'.join( f'\t- :class:`{event.__module__}.{event.__qualname__}`' for event in manifest.events ) + '\n\n', ) return idx + 1 @staticmethod def add_actions(source: list[str], manifest: IntegrationMetadata, idx: int) -> int: if not (manifest.actions and manifest.cls): return idx source.insert( idx, 'Actions\n-------\n\n' + '\n'.join( f'\t- `{get_plugin_name_by_class(manifest.cls)}.{action} ' + f'<#{manifest.cls.__module__}.{manifest.cls.__qualname__}.{action}>`_' for action in sorted(manifest.actions.keys()) ) + '\n\n', ) return idx + 1 @staticmethod def _shellify(title: str, cmd: str) -> str: return f'**{title}**\n\n' + '.. code-block:: bash\n\n\t' + cmd + '\n\n' @classmethod def add_install_deps( cls, source: list[str], manifest: IntegrationMetadata, idx: int ) -> int: deps = manifest.deps parsed_deps = { 'before': deps.before, 'pip': deps.pip, 'after': deps.after, } if not (any(parsed_deps.values()) or deps.by_pkg_manager): return idx source.insert(idx, 'Dependencies\n------------\n\n') idx += 1 if parsed_deps['before']: source.insert(idx, cls._shellify('Pre-install', '\n'.join(deps.before))) idx += 1 if parsed_deps['pip']: source.insert(idx, cls._shellify('pip', 'pip ' + ' '.join(deps.pip))) idx += 1 for pkg_manager, sys_deps in deps.by_pkg_manager.items(): if not sys_deps: continue source.insert( idx, cls._shellify( pkg_manager.value.default_os.value.description, pkg_manager.value.install_doc + ' ' + ' '.join(sys_deps), ), ) idx += 1 if parsed_deps['after']: source.insert(idx, cls._shellify('Post-install', '\n'.join(deps.after))) idx += 1 return idx @classmethod def add_description( cls, source: list[str], manifest: IntegrationMetadata, idx: int ) -> int: docs = ( doc for doc in ( inspect.getdoc(manifest.cls) or '', manifest.constructor.doc if manifest.constructor else '', ) if doc ) if not docs: return idx docstring = '\n\n'.join(docs) source.insert(idx, f"Description\n-----------\n\n{docstring}\n\n") return idx + 1 @classmethod def add_conf_snippet( cls, source: list[str], manifest: IntegrationMetadata, idx: int ) -> int: source.insert( idx, tw.dedent( f""" Configuration ------------- .. code-block:: yaml {tw.indent(manifest.config_snippet, ' ')} """ ), ) return idx + 1 def __call__(self, _: Sphinx, doc: str, source: list[str]): if not (source and re.match(r'^platypush/(backend|plugins)/.*', doc)): return src = [src.split('\n') for src in source][0] if len(src) < 3: return manifest_file = os.path.join( base_path, *doc.split(os.sep)[:-1], *doc.split(os.sep)[-1].split('.'), 'manifest.yaml', ) if not os.path.isfile(manifest_file): return with mock_imports(): manifest = IntegrationMetadata.from_manifest(manifest_file) idx = self.add_description(src, manifest, idx=3) idx = self.add_conf_snippet(src, manifest, idx=idx) idx = self.add_install_deps(src, manifest, idx=idx) idx = self.add_events(src, manifest, idx=idx) idx = self.add_actions(src, manifest, idx=idx) src.insert(idx, '\n\nModule reference\n----------------\n\n') source[0] = '\n'.join(src) @contextmanager def mock_imports(): conf_mod = import_file(os.path.join(base_path, 'docs', 'source', 'conf.py')) mock_mods = getattr(conf_mod, 'autodoc_mock_imports', []) with mock(*mock_mods): yield def setup(app: Sphinx): app.connect('source-read', IntegrationEnricher()) return { 'version': '0.1', 'parallel_read_safe': True, 'parallel_write_safe': True, }