Files
openlane2/docs/_ext/generate_module_autodocs.py
Mohamed Gaber 315e8a220f The Override Overhaul (#482)
## CLI
* Overhauled how the PDK commandline options work, using a decorator instead of doing everything in a callback
* `--smoke-test/--run-example` are now no longer callbacks, and `--run-example` now supports more options (e.g. another PDK, another flow, etc.)


## Steps
* Created `OpenROAD.DEFtoODB`
  * Useful for custom flows, where the DEF is modified but the ODB needs to be updated to reflect these modifications

## Flows
* `VHDLClassic` is now `Classic` with appropriate Substitutions, see Misc.

## Misc Enhancements/Updates
* `SequentialFlow`
  * Substitutions can now
    * be done at the class level by assigning to `Substitutions`
    * be done in `config.json` files using a dictionary in the field `.meta.substituting_steps`
    * emplace steps before or after existing steps, e.g. `+STEP`, `-STEP`
  * Step names for `from`, `to`, `skip` and `only` are now fuzzy-matched using `rapidfuzz` to give suggestions in error messages
    * If the environment variable `_i_want_openlane_to_fuzzy_match_steps_and_im_willing_to_accept_the_risks` is set to `1`, the suggestions are used automatically (not recommended)
  * Gating config vars are now simply removed if they do not target a valid step (so removed steps in a substituted flow do not cause a FlowException)

## Documentation
* Updated the architecture document to reflect changes and clarify some elements.
* Updated Usage/Writing Custom Flows to document step substitution
* Created a new document on writing plugins

## Tool Updates
* Upgrade `nix-eda`
  * `forAllSystems` now composes overlays for nixpkgs based on the `withInputs` field, allowing for easier overriding
  * `nixpkgs` -> 24.05
  * `klayout` -> `0.29.1`
  * `ioplace_parser` -> `0.3.0`
* Python build tool changed from `setuptools` to `poetry`, which properly verifies that all version ranges are within constraints
  * Updated wrong Python package version ranges that all happen to work

* Nix devshells now use [numtide/devshell](https://github.com/numtide/devshell), which creates an executable to enter the environment, allowing for easy repacking
2024-07-02 09:06:53 +00:00

214 lines
6.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2024 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
import os
import inspect
import traceback
import importlib
from types import ModuleType
from typing import List, Tuple, Dict, Optional, Set
import jinja2
from sphinx.config import Config
from sphinx.application import Sphinx
from docstring_parser import parse, Docstring
from util import debug, rimraf, mkdirp
def setup(app: Sphinx):
app.connect("config-inited", generate_module_docs)
app.add_config_value("generate_module_autodocs", [], True)
return {"version": "1.0"}
def generate_docs_for_module(
processed: Set[ModuleType],
build_path: str,
templates: Dict[str, jinja2.Template],
top_level_name: Optional[str],
top_level_dir: Optional[str],
module: ModuleType,
docstring: Docstring,
):
if module in processed:
return
debug(f"Processing {module.__name__}")
processed.add(module)
template = templates["module"]
assert module.__file__ is not None
full_name = module.__name__
module_path = full_name.split(".")
module_parents = ".".join(module_path[:-1])
module_name = module_path[-1]
module_file = os.path.abspath(inspect.getfile(module))
top_level_name = top_level_name or full_name
top_level_dir = top_level_dir or os.path.dirname(module_file)
module_file_rel = os.path.relpath(module_file, top_level_dir)
if module_file_rel.endswith("__init__.py"):
module_file_rel = module_file_rel[: -len("__init__.py")]
elif module_file_rel.endswith(".py"):
module_file_rel = module_file_rel[:-3]
module_doc_dir = os.path.abspath(os.path.join(build_path, module_file_rel))
module_doc_path = os.path.join(module_doc_dir, "index")
module_rst_path = os.path.join(module_doc_dir, "index.rst")
submodules = []
# Process Submodules
for key in dir(module):
attr = getattr(module, key)
try:
if attr in processed:
continue
object_full_name = attr.__name__
object_path = object_full_name.split(".")
object_name = object_path[-1]
object_file = os.path.abspath(inspect.getfile(attr))
except (TypeError, AttributeError):
continue
if not object_file.startswith(top_level_dir):
continue
docstring_raw = None
try:
docstring_raw = getattr(attr, "__doc__")
except AttributeError:
pass
if docstring_raw is None:
debug(f"Skipping {object_full_name}: no docstring")
continue
object_docstring = parse(docstring_raw)
if inspect.ismodule(attr):
submodule = attr
if submodule.__doc__ is None:
continue
submodule_doc_path = generate_docs_for_module(
processed=processed,
module=submodule,
docstring=object_docstring,
build_path=build_path,
top_level_name=top_level_name,
top_level_dir=top_level_dir,
templates=templates,
)
submodule_relative_path = os.path.relpath(
submodule_doc_path,
module_doc_dir,
)
submodules.append(
(
object_name,
object_docstring.short_description,
submodule_relative_path,
)
)
short_desc = docstring.short_description
long_desc = docstring.long_description
include_imported_members = True
if "no-imported-members" in long_desc:
include_imported_members = False
# Process Current Module
kwargs = {
"parent_name": module_parents,
"full_name": full_name,
"module_name": module_name,
"short_desc": short_desc,
"long_desc": long_desc,
"submodules": submodules,
"include_imported_members": include_imported_members,
}
mkdirp(os.path.dirname(module_rst_path))
with open(module_rst_path, "w") as f:
f.write(template.render(**kwargs))
return module_doc_path
def generate_module_docs(app: Sphinx, conf: Config):
try:
generate_module_autodocs_conf: List[Tuple[str, str]] = (
conf.generate_module_autodocs
)
conf_py_path: str = conf._raw_config["__file__"]
doc_root_dir: str = os.path.dirname(conf_py_path)
template_relpath: str = conf.templates_path[0]
all_templates_path = os.path.abspath(template_relpath)
template_path = os.path.join(all_templates_path, "generate_module_autodocs")
lookup = jinja2.FileSystemLoader(searchpath=template_path)
# Mako-like environment
env = jinja2.Environment(
"<%",
"%>",
"${",
"}",
"<%doc>",
"</%doc>",
"%",
"##",
loader=lookup,
)
templates = {k: env.get_template(f"{k}.rst") for k in ["module"]}
for module_name, build_path in generate_module_autodocs_conf:
rimraf(build_path)
build_path_resolved = os.path.join(doc_root_dir, build_path)
top_level_module = importlib.import_module(module_name)
try:
docstring_raw = getattr(top_level_module, "__doc__")
except AttributeError:
raise ValueError("Top level module lacks a docstring.")
docstring = parse(docstring_raw)
generate_docs_for_module(
processed=set(),
module=top_level_module,
docstring=docstring,
build_path=build_path_resolved,
templates=templates,
top_level_name=None,
top_level_dir=None,
)
except Exception:
print(traceback.format_exc())
exit(-1)