Thanks to visit codestin.com
Credit goes to github.com

Skip to content

📝 Update includes format in docs with an automated script#12950

Merged
tiangolo merged 17 commits into
masterfrom
autoinclude
Nov 18, 2024
Merged

📝 Update includes format in docs with an automated script#12950
tiangolo merged 17 commits into
masterfrom
autoinclude

Conversation

@tiangolo

@tiangolo tiangolo commented Nov 17, 2024

Copy link
Copy Markdown
Member

📝 Update includes format in docs with an automated script

The files were generated with a script taking inspiration and pieces of code from #12896 by @AlexWendland

The script was built iteratively fixing each use case (edge case 😅).

The changes were reviewed by hand, one by one, line by line.

THe script:

import re
from pathlib import Path
from typing import Optional, Tuple

import typer
from pydantic import BaseModel


class InvalidFormatError(Exception):
    """
    Invalid format
    """


def get_hl_lines(old_first_line: str) -> Optional[str]:
    """
    Takes lines such as:
    ```Python hl_lines="3"
    and extracts the hl_ines part and returns the new hlines string "3" in this example.
    For groups of likes such as
    ```Python hl_lines="5-10"
    it needs to convert the - to a : so in this example it would be "5:10".
    For multiple lines it needs to seperate them by comma, such as:
    ```Python hl_lines="3   5-10   12"
    would be "3,5:10,12".
    For examples with no hlighted lines such as
    ```Python
    it should return None.
    """
    old_hl = re.search(r"hl_lines=\"(.*)\"", old_first_line)
    if old_hl is None:
        return None
    old_hl = old_hl.group(1)
    hl_lines = []
    for line in old_hl.split():
        if "-" in line:
            hl_lines.append(replace_hypen_with_colon(line))
        else:
            hl_lines.append(line)
    return ",".join(hl_lines)


def replace_hypen_with_colon(line: str) -> str:
    start, end = line.split("-")
    return f"{start}:{end}"


def convert_file_reference(reference_line: str) -> Tuple[str, Optional[str]]:
    """
    This takes a line such as:
        {!> ../../docs_src/security/tutorial006_an_py39.py!}
    and extracts the file reference such as ../../docs_src/security/tutorial006_an_py39.py.
    This can also accept file references with a line reference such as
    {!> ../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!}
    in this case it will return the file reference ../../docs_src/separate_openapi_schemas/tutorial001_py310.py and the line reference 1:7 (where we have replaced - with a :).
    """
    file_ref = re.search(r"../(.*).py", reference_line)
    if file_ref is None:
        raise InvalidFormatError("Invalid file reference.")
    file_ref = file_ref.group(0)
    line_ref = re.search(r"\[ln:(.*)\]", reference_line)
    if line_ref is None:
        return file_ref, None
    line_ref = line_ref.group(1)
    if "-" not in line_ref:
        return file_ref, line_ref
    return file_ref, replace_hypen_with_colon(line_ref)


PYTHON_BLOCK = "```Python"
END_BLOCK = "```"
PYTHON_TAB = ("//// tab | Python", "//// tab | 🐍", "//// tab | 파이썬")
TAB_END = "////"


class FencedPython(BaseModel):
    first_line: str
    body: list[str]

    def __str__(self) -> str:
        formatted_body_lines = format_fenced_python(self)
        body = "\n".join(formatted_body_lines)
        return body


class Tab(BaseModel):
    first_line: str
    body: list[str]

    def __str__(self) -> str:
        body = "\n".join(self.body).strip()
        return f"{self.first_line}\n\n{body}\n\n{TAB_END}"


class TabGroup(BaseModel):
    tabs: list[Tab]

    def __str__(self) -> str:
        return format_tab_group(self)


def format_fenced_python(block: FencedPython) -> list[str]:
    if block.body[0].startswith("{!"):
        hlines = get_hl_lines(block.first_line)
        file_ref, line_ref = convert_file_reference(block.body[0])
        return [
            "{* "
            + f"{file_ref} {f'ln[{line_ref}] ' if line_ref else ''}{f'hl[{hlines}] ' if hlines else ''}"
            + "*}"
        ]
    return [block.first_line] + block.body + [END_BLOCK]


def format_tab_group(group: TabGroup) -> str:
    first_tab = group.tabs[0]
    fenced_block: FencedPython | None = None
    for line in first_tab.body:
        if fenced_block:
            if line == END_BLOCK:
                if fenced_block.body[0].startswith("{!"):
                    return str(fenced_block)
                return "\n\n".join(str(tab) for tab in group.tabs)
            fenced_block.body.append(line)
            continue
        if line and not line.startswith(PYTHON_BLOCK):
            return "\n\n".join(str(tab) for tab in group.tabs)
        if line.startswith(PYTHON_BLOCK):
            fenced_block = FencedPython(first_line=line, body=[])
    raise RuntimeError("Invalid tab group")


def process_lines(lines: list[str]) -> list[str | FencedPython | Tab]:
    new_sections: list[str | FencedPython | Tab] = []
    fenced_block: FencedPython | None = None
    tab: Tab | None = None

    for line in lines:
        if not fenced_block and not tab:
            if line.startswith(PYTHON_BLOCK):
                fenced_block = FencedPython(first_line=line, body=[])
                continue
            if line.startswith(PYTHON_TAB):
                tab = Tab(first_line=line, body=[])
                continue
            new_sections.append(line)
            continue
        if fenced_block:
            if line == END_BLOCK:
                new_sections.append(fenced_block)
                fenced_block = None
                continue
            fenced_block.body.append(line)
            continue
        if tab:
            if line == TAB_END:
                new_sections.append(tab)
                tab = None
                continue
            tab.body.append(line)
            continue
    return new_sections


def process_fragments(
    fragments: list[str | FencedPython | Tab],
) -> list[str | FencedPython | TabGroup]:
    new_fragments: list[str | FencedPython | TabGroup] = []
    tab_group: TabGroup | None = None
    for fragment in fragments:
        if not tab_group:
            if isinstance(fragment, Tab):
                tab_group = TabGroup(tabs=[fragment])
                continue
            new_fragments.append(fragment)
            continue
        if tab_group:
            if isinstance(fragment, Tab):
                tab_group.tabs.append(fragment)
                continue
            if fragment == "":
                continue
            new_fragments.append(tab_group)
            new_fragments.append("")
            tab_group = None
            new_fragments.append(fragment)
            continue
    if tab_group:
        new_fragments.append(tab_group)
    return new_fragments


def process_file(file_path: Path) -> None:
    lines = file_path.read_text().splitlines()
    sections = process_lines(lines)
    groups = process_fragments(sections)
    group_str = [str(group) for group in groups]
    file_path.write_text("\n".join(group_str).strip() + "\n")


skip_file_names = ["python-types.md", "bigger-applications.md", "testing.md"]


def main(file_path: Path | None = None) -> None:
    if file_path:
        process_file(file_path)
        return
    files_with_errors = []
    for f in Path("docs/").glob("**/*.md"):
        if f.name in skip_file_names:
            continue
        try:
            process_file(f)
        except Exception as e:
            print(f"An error occurred in file {f}: {e}")
            files_with_errors.append(f)
    print("Files with errors:")
    for f in files_with_errors:
        print(f)


if __name__ == "__main__":
    typer.run(main)

@github-actions github-actions Bot added the docs Documentation about how to use FastAPI label Nov 17, 2024
@github-actions

This comment was marked as resolved.

@github-actions

This comment was marked as resolved.

@github-actions

This comment was marked as resolved.

@github-actions

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown
Contributor

📝 Docs preview for commit 0ed2166 at: https://bdf0dfc9.fastapitiangolo.pages.dev

Modified Pages

@tiangolo tiangolo marked this pull request as ready for review November 18, 2024 02:25
@tiangolo tiangolo merged commit 1c711e7 into master Nov 18, 2024
@tiangolo tiangolo deleted the autoinclude branch November 18, 2024 02:25
s-rigaud pushed a commit to s-rigaud/fastapi that referenced this pull request Jan 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation about how to use FastAPI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant