diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000000..d07c40d19e5 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,4 @@ +.wy-nav-content { + max-width: none; + overflow-x: auto; +} diff --git a/docs/conf.py b/docs/conf.py index 68910c8ada1..50fc8b642dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,7 @@ import os import shutil +import subprocess import sys # If extensions (or modules to document with autodoc) are in another directory, @@ -21,7 +22,7 @@ sys.path.insert(0, os.path.abspath('..')) # configuration required to import test modules -for path in ["../pyanaconda", "../tests", "../tests/lib", "../dracut", "../widgets"]: +for path in ["../pyanaconda", "../tests", "../tests/lib", "../dracut", "../widgets", "../pyanaconda/modules"]: sys.path.append(os.path.abspath(path)) # -- General configuration ----------------------------------------------------- @@ -32,8 +33,16 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] -if not os.environ.get("READTHEDOCS") == "True": - extensions.extend(['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.todo']) +extensions.extend(['sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'sphinx.ext.inheritance_diagram', + 'sphinx.ext.todo', + 'sphinx_autodoc_typehints', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'autoapi.extension', + ]) shutil.copy2("../CONTRIBUTING.rst", "contributing.rst") @@ -117,12 +126,21 @@ def read_version(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + 'navigation_depth': 4, + 'collapse_navigation': True, + 'sticky_navigation': True, + 'prev_next_buttons_location': 'both', +} + +html_css_files = [ + 'custom.css', +] # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -146,7 +164,7 @@ def read_version(): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -312,7 +330,9 @@ def read_version(): # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/3': None} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', 'https://docs.python.org/3/objects.inv') +} # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' @@ -323,6 +343,46 @@ def read_version(): html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Group by class -autodoc_member_order = 'source' +autodoc_member_order = 'bysource' + +# AutoAPI configuration +autoapi_type = 'python' +autoapi_dirs = ['../pyanaconda/modules'] +autoapi_add_toctree_entry = False +autoapi_file_patterns = ['*_interface.py'] +autoapi_mock_imports = [ + 'gi.repository', + 'dasbus', + 'pydbus', + 'dbus', + 'blivet', + 'pykickstart', + 'pyanaconda.core', + 'pyanaconda.anaconda_loggers' +] + +autoapi_options = [ + 'members', + 'undoc-members', + 'show-inheritance', + 'show-module-summary', + 'special-members: __init__', + 'private-members', +] + +autoapi_python_class_content = 'both' +autodoc_typehints = "description" + +def generate_modules_rst(app): + """ + Runs the generate_modules.py script after AutoAPI has generated documentation. + + It is triggered by "builder-inited", ensuring that AutoAPI files exist + before structuring them in modules.rst. + """ + print("[Sphinx] Waiting for AutoAPI to generate documentation...") + subprocess.run(["python", "generate_modules.py"], check=True) + print("[Sphinx] modules.rst generated successfully.") -# otherwise, readthedocs.org uses their theme by default, so no need to specify it +def setup(app): + app.connect("builder-inited", generate_modules_rst) diff --git a/docs/generate_modules.py b/docs/generate_modules.py new file mode 100644 index 00000000000..a6a9e433303 --- /dev/null +++ b/docs/generate_modules.py @@ -0,0 +1,193 @@ +import os +import re +from collections import defaultdict + +DOCS_PATH = "autoapi/pyanaconda/modules" +OUTPUT_FILE = "modules.rst" + +header = """Anaconda DBUS Modules +===================== + +This page contains the D-Bus API documentation, grouped by modules. + +.. toctree:: + :maxdepth: 2 + +""" + + +def submodule_name_to_class_name(submodule_name: str) -> str: + """ + Convert a submodule name (e.g. 'network') to a DBus interface class name + (e.g. 'NetworkInterface'). + """ + parts = submodule_name.split("_") + capitalized = [p.capitalize() for p in parts] + return "".join(capitalized) + "Interface" + + +def extract_summary_from_rst(index_rst_path, class_name): + """ + Search in the given RST file for the line: .. py:class:: + Then take the first suitable indented line as a docstring: + - At least 3 spaces indentation + - Not empty + - Does not start with 'Bases:' + Returns the found summary (string) or None if none found. + """ + if not os.path.isfile(index_rst_path): + return None + + class_directive_regex = re.compile(rf"^\.\.\s+py:class::\s+{class_name}\s*$") + next_directive_regex = re.compile(r"^\.\.\s+py:") + + with open(index_rst_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + found_class = False + min_indent = 1 + + for i, line in enumerate(lines): + striped_line = line.rstrip("\n") + + if not found_class: + if class_directive_regex.match(striped_line): + found_class = True + continue + else: + if next_directive_regex.match(striped_line): + break + + match_indent = re.match(r"^(\s+)(.*)$", line) + if not match_indent: + break + + indent_str, text = match_indent.groups() + indent_len = len(indent_str) + if indent_len < min_indent: + continue + + text = text.strip() + if not text: + continue + if text.startswith("Bases:"): + continue + + return text + + return None + + +def build_module_tree(): + """ + Build a nested structure of modules and their interfaces: + { + 'submodules': { + 'boss': { + 'submodules': {...}, + 'interfaces': { + 'autoapi/pyanaconda/modules/boss/boss_interface/index': 'docstring' + } + } + }, + 'interfaces': {...} + } + """ + def create_node(): + return { + "interfaces": {}, + "submodules": defaultdict(create_node), + } + + tree = create_node() + + for root, dirs, files in os.walk(DOCS_PATH): + if not root.endswith("_interface"): + continue + + rel_path = os.path.relpath(root, DOCS_PATH) + parts = rel_path.split(os.sep) + last_part = parts[-1] + if not last_part.endswith("_interface"): + continue + + submodule_name = last_part[:-10] # remove "_interface" + class_name = submodule_name_to_class_name(submodule_name) + + index_rst_file = os.path.join(root, "index.rst") + if not os.path.exists(index_rst_file): + index_rst_file = os.path.join(root, "index") + if not os.path.exists(index_rst_file): + continue + + docstring_summary = extract_summary_from_rst(index_rst_file, class_name) + + parent_parts = parts[:-1] + current = tree + for p in parent_parts: + if p.endswith("_interface"): + p = p[:-10] + current = current["submodules"][p] + + interface_doc_ref = os.path.join(root, "index").replace(os.sep, "/") + current["interfaces"][interface_doc_ref] = docstring_summary + + return tree + + +def write_tree(f, node, current_module=None, depth=0): + """ + Recursively write the content of the module tree to the output file. + + - depth=0: root level. Prints any interfaces found directly under the root, if any. + - depth=1: top-level modules. Prints the module name, and the summary for the interface + matching the module name (e.g. 'network_interface/index'). + A toctree listing references to all interfaces (including submodules) follows. + - depth>1: recursion stops (you can adjust if deeper levels are needed). + """ + if current_module is None: + current_module = [] + + if depth > 1: + return + + module_name = ".".join(current_module) + + if depth == 1: + def collect_interfaces(n): + interfaces = dict(n["interfaces"]) + for sub in n["submodules"].values(): + interfaces.update(collect_interfaces(sub)) + return interfaces + aggregated_interfaces = collect_interfaces(node) + else: + aggregated_interfaces = node["interfaces"] + + if aggregated_interfaces: + underline = "=" if depth == 0 else "-" if depth == 1 else "~" + + if module_name: + f.write(f"\n{module_name}\n{underline * len(module_name)}\n") + + if depth == 1 and module_name: + main_submodule_name = current_module[-1] + main_interface_suffix = f"{main_submodule_name}_interface/index" + for path, summary in aggregated_interfaces.items(): + if path.endswith(main_interface_suffix) and summary: + f.write(f"\n{summary}\n") + break + + f.write("\n.. toctree::\n :maxdepth: 1\n\n") + for path in sorted(aggregated_interfaces.keys()): + f.write(f" {path}\n") + + if depth < 1: + for sub_name in sorted(node["submodules"]): + write_tree(f, node["submodules"][sub_name], current_module + [sub_name], depth + 1) + + +if __name__ == "__main__": + module_tree = build_module_tree() + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + f.write(header) + write_tree(f, module_tree) diff --git a/docs/index.rst b/docs/index.rst index 44ffe3cd50e..772274c9f05 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,4 +21,5 @@ Contents: release list-harddrives developer + modules ci-status diff --git a/docs/requirements.txt b/docs/requirements.txt index eed77b15127..e074948101b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,7 @@ # Declare Python requirements required to build your docs. sphinx>=4.2.0 +sphinx-autoapi>=1.9.0 sphinx_rtd_theme>=1.0.0 +sphinx-autodoc-typehints>=1.12.0 +sphinxcontrib-napoleon>=0.7 +graphviz>=0.20.0