|
| 1 | +import ast |
| 2 | +from pathlib import Path |
| 3 | +from typing import TypedDict |
| 4 | + |
| 5 | +from jinja2 import Environment, FileSystemLoader |
| 6 | + |
| 7 | +from metatrain.utils import hooks as hooks_module |
| 8 | +from metatrain.utils.hooks.helpers import ( |
| 9 | + find_all_hooks, |
| 10 | + get_hypers_class, |
| 11 | + preload_documentation_module, |
| 12 | + write_hypers_yaml, |
| 13 | +) |
| 14 | +from metatrain.utils.hypers import get_hypers_list |
| 15 | + |
| 16 | + |
| 17 | +HOOKS_DIR = Path(__file__).parent |
| 18 | +TEMPLATES_DIR = HOOKS_DIR / "templates" |
| 19 | +DEFAULT_HYPERS_DIR = HOOKS_DIR / "default_hypers" |
| 20 | +GENERATED_DIR = HOOKS_DIR / "generated" |
| 21 | + |
| 22 | + |
| 23 | +JINJA_ENV = Environment( |
| 24 | + loader=FileSystemLoader(TEMPLATES_DIR), |
| 25 | + trim_blocks=True, |
| 26 | + lstrip_blocks=True, |
| 27 | +) |
| 28 | + |
| 29 | + |
| 30 | +SECTIONS = [ |
| 31 | + "installation", |
| 32 | + "hook_hypers", |
| 33 | + "references", |
| 34 | +] |
| 35 | + |
| 36 | + |
| 37 | +class HookDocVariables(TypedDict): |
| 38 | + """Variables to use inside the hook documentation. |
| 39 | +
|
| 40 | + The docstring of the hook will be processed as a |
| 41 | + ``jinja`` template. You can find documentation about them |
| 42 | + `here <https://jinja.palletsprojects.com/en/stable/templates>`_ , but |
| 43 | + the simplest functionality consists of using variables enclosed in |
| 44 | + double curly braces ``{{variable_name}}``, which will be replaced by |
| 45 | + their corresponding value. |
| 46 | +
|
| 47 | + For example, a file with the following content: |
| 48 | +
|
| 49 | + .. code-block:: rst |
| 50 | +
|
| 51 | + This is the documentation for {{hook}}. |
| 52 | +
|
| 53 | + generates a documentation file that for the hook ``tensor_basis`` would be: |
| 54 | +
|
| 55 | + .. code-block:: rst |
| 56 | +
|
| 57 | + This is the documentation for tensor_basis. |
| 58 | +
|
| 59 | + There are some special variables that start with ``SECTION_``. These contain |
| 60 | + the content of different sections of the documentation, and they will be |
| 61 | + appended to the docstring if they are not already present. For example, given |
| 62 | + the docstring: |
| 63 | +
|
| 64 | + .. code-block:: python |
| 65 | +
|
| 66 | + \""" |
| 67 | + My hook |
| 68 | + ======= |
| 69 | +
|
| 70 | + This is my hook. |
| 71 | +
|
| 72 | + {{SECTION_DEFAULT_HYPERS}} |
| 73 | +
|
| 74 | + Some important section |
| 75 | + ====================== |
| 76 | +
|
| 77 | + Explain something important here. |
| 78 | + \""" |
| 79 | +
|
| 80 | + The final documentation will append to the docstring all the sections except |
| 81 | + ``SECTION_DEFAULT_HYPERS``, since it is already present. |
| 82 | +
|
| 83 | + Following you can find a description of all the available variables. The |
| 84 | + sections are appended in the order documented here. |
| 85 | + """ |
| 86 | + |
| 87 | + SECTION_INSTALLATION: str |
| 88 | + """Section containing installation instructions for this hook.""" |
| 89 | + SECTION_HOOK_HYPERS: str |
| 90 | + """Section containing the description of the hook hyperparameters for |
| 91 | + this hook.""" |
| 92 | + SECTION_REFERENCES: str |
| 93 | + """Section containing references for this hook. It will render the |
| 94 | + references that have been used as ``:footcite:p:`` during the hook |
| 95 | + documentation.""" |
| 96 | + |
| 97 | + hook: str |
| 98 | + """The name of the hook. |
| 99 | +
|
| 100 | + This excludes any 'experimental.' or 'deprecated.' prefix.""" |
| 101 | + default_hypers_path: str |
| 102 | + """Path to the yaml file with the default hyperparameters for this |
| 103 | + hook. |
| 104 | +
|
| 105 | + This is a path relative to the ``docs/src/hooks/generated`` |
| 106 | + directory. |
| 107 | + """ |
| 108 | + hook_hypers_path: str |
| 109 | + """The full python import path to the hook's hypers class of this |
| 110 | + hook. |
| 111 | +
|
| 112 | + E.g.: ``"metatrain.utils.hooks.<hook_name>.Hypers"`` |
| 113 | + """ |
| 114 | + hook_hypers: list[str] |
| 115 | + """List of hyperparameter names for this hook.""" |
| 116 | + |
| 117 | + |
| 118 | +def setup_hooks_docs(): |
| 119 | + """Generate the hook documentation files. |
| 120 | +
|
| 121 | + This function goes through all available hooks, and for each of them |
| 122 | + generates a yaml file with the default hyperparameters (so that it can be |
| 123 | + easily included in the documentation) and their rst documentation file. |
| 124 | +
|
| 125 | + See :ref:`newarchitecture-documentation-page` for more information. |
| 126 | + """ |
| 127 | + # If the default_hypers directory does not exist, create it |
| 128 | + DEFAULT_HYPERS_DIR.mkdir(exist_ok=True) |
| 129 | + # Same for the generated directory |
| 130 | + GENERATED_DIR.mkdir(exist_ok=True) |
| 131 | + |
| 132 | + for name in find_all_hooks(): |
| 133 | + # Load documentation module in an isolated way to avoid |
| 134 | + # requiring dependencies for every architecture. |
| 135 | + preload_documentation_module(name) |
| 136 | + |
| 137 | + # Write default hypers file |
| 138 | + yaml_path = DEFAULT_HYPERS_DIR / f"{name}-default-hypers.yaml" |
| 139 | + write_hypers_yaml(name, yaml_path, include_name=True) |
| 140 | + |
| 141 | + generate_rst(name, yaml_path=yaml_path) |
| 142 | + |
| 143 | + |
| 144 | +def generate_rst( |
| 145 | + hook_name: str, |
| 146 | + yaml_path: Path, |
| 147 | +): |
| 148 | + """Generate the rst documentation file for a given hook. |
| 149 | +
|
| 150 | + :param hook_name: The name of the hook to generate the |
| 151 | + documentation for. |
| 152 | + :param yaml_path: Path to the yaml file with the default hyperparameters |
| 153 | + for this architecture. |
| 154 | + """ |
| 155 | + |
| 156 | + # Get the full python import path to the hook |
| 157 | + hook_path = f"metatrain.utils.hooks.{hook_name}" |
| 158 | + |
| 159 | + # Get the docstring from the documentation.py file |
| 160 | + doc_file = Path(hooks_module.__file__).parent / hook_name / "documentation.py" |
| 161 | + with open(doc_file, "r") as f: |
| 162 | + module = ast.parse(f.read(), filename=str(doc_file)) |
| 163 | + docstring = ast.get_docstring(module) |
| 164 | + if docstring is None: |
| 165 | + raise ValueError( |
| 166 | + f"The documentation.py file for hook " |
| 167 | + f"'{hook_name}' does not have a module docstring." |
| 168 | + ) |
| 169 | + |
| 170 | + hypers_class = get_hypers_class(hook_name) |
| 171 | + |
| 172 | + # Prepare template variables |
| 173 | + template_variables = dict( |
| 174 | + hook=hook_name, |
| 175 | + default_hypers_path=".." / yaml_path.relative_to(HOOKS_DIR), |
| 176 | + hook_hypers_path=f"{hook_path}.documentation.Hypers", |
| 177 | + hook_hypers=get_hypers_list(hypers_class), |
| 178 | + ) |
| 179 | + |
| 180 | + # Read section templates and render them |
| 181 | + for section in SECTIONS: |
| 182 | + template = JINJA_ENV.get_template(f"{section}.rst") |
| 183 | + template_variables[f"SECTION_{section.upper()}"] = template.render( |
| 184 | + **template_variables |
| 185 | + ) |
| 186 | + |
| 187 | + # Check for missing sections and add them to the end of the docstring |
| 188 | + for section in SECTIONS: |
| 189 | + section_var = "{{SECTION_" + section.upper() + "}}" |
| 190 | + if section_var not in docstring: |
| 191 | + docstring += f"\n\n{section_var}" |
| 192 | + |
| 193 | + # Render docstring template |
| 194 | + docstring = JINJA_ENV.from_string(docstring).render(**template_variables) |
| 195 | + |
| 196 | + # Write to file |
| 197 | + with open(GENERATED_DIR / f"{hook_name}.rst", "w") as f: |
| 198 | + f.write(docstring + "\n") |
0 commit comments