Files
mlflow--mlflow/docs/scripts/convert-notebooks.py
T
2026-07-13 13:22:34 +08:00

118 lines
3.5 KiB
Python

"""
Converts all .ipynb files from the docs/ folder into .mdx files.
This script uses nbconvert to do the processing.
"""
import multiprocessing
import re
from pathlib import Path
import nbformat
import yaml
from nbconvert.exporters import MarkdownExporter
from nbconvert.preprocessors import Preprocessor
SOURCE_DIR = Path("docs/")
NOTEBOOK_BASE_EDIT_URL = "https://github.com/mlflow/mlflow/edit/master/docs/"
NOTEBOOK_BASE_DOWNLOAD_URL = "https://raw.githubusercontent.com/mlflow/mlflow/master/docs/"
class EscapeBackticksPreprocessor(Preprocessor):
def preprocess_cell(self, cell, resources, cell_index):
if cell.cell_type == "code":
# escape backticks, as code blocks will be rendered
# inside a custom react component like:
# <NotebookCellOutput>`{{ content }}`</NotebookCellOutput>
# and having the backticks causes issues
cell.source = cell.source.replace("`", r"\`")
if "outputs" in cell:
for i, output in enumerate(cell["outputs"]):
if "text" in output:
output["text"] = output["text"].replace("`", r"\`")
elif "data" in output:
for key, value in output["data"].items():
if isinstance(value, str):
output["data"][key] = value.replace("`", r"\`")
elif cell.cell_type == "raw":
cell.source = cell.source.replace("<br>", "<br />")
return cell, resources
exporter = MarkdownExporter(
preprocessors=[EscapeBackticksPreprocessor],
template_name="mdx",
extra_template_basedirs=["./scripts/nbconvert_templates"],
)
def add_frontmatter(
body: str,
nb_path: Path,
) -> str:
frontmatter = {
"custom_edit_url": NOTEBOOK_BASE_EDIT_URL + str(nb_path),
"slug": nb_path.stem,
}
formatted_frontmatter = yaml.dump(frontmatter)
return f"""---
{formatted_frontmatter}
---
{body}"""
def add_download_button(
body: str,
nb_path: Path,
) -> str:
download_url = NOTEBOOK_BASE_DOWNLOAD_URL + str(nb_path)
download_button = f'<NotebookDownloadButton href="{download_url}">Download this notebook</NotebookDownloadButton>'
# Insert the notebook underneath the first H1 header (assumed to be the title)
pattern = r"(^#\s+.+$)"
return re.sub(pattern, rf"\1\n\n{download_button}", body, count=1, flags=re.MULTILINE)
# add the imports for our custom cell output components
def add_custom_component_imports(
body: str,
) -> str:
return f"""import {{ NotebookCodeCell }} from "@site/src/components/NotebookCodeCell"
import {{ NotebookCellOutput }} from "@site/src/components/NotebookCellOutput"
import {{ NotebookHTMLOutput }} from "@site/src/components/NotebookHTMLOutput"
import {{ NotebookDownloadButton }} from "@site/src/components/NotebookDownloadButton"
{body}
"""
def convert_path(nb_path: Path):
mdx_path = nb_path.with_stem(nb_path.stem + "-ipynb").with_suffix(".mdx")
with open(nb_path) as f:
nb = nbformat.read(f, as_version=4)
body, _ = exporter.from_notebook_node(nb)
body = add_custom_component_imports(body)
body = add_frontmatter(body, nb_path)
body = add_download_button(body, nb_path)
with open(mdx_path, "w") as f:
f.write(body)
return mdx_path
def main():
nb_paths = list(SOURCE_DIR.rglob("*.ipynb"))
with multiprocessing.Pool() as pool:
pool.map(convert_path, nb_paths)
if __name__ == "__main__":
main()