From 3a0e4233fa702ef1d3b9ffa0c8a30c98cc082437 Mon Sep 17 00:00:00 2001 From: "Felt, Nicholas" Date: Tue, 22 Oct 2024 07:41:32 -0700 Subject: [PATCH] docs: Update documentation templates and macros --- .../python/readthedocs/children.html.jinja | 16 +-- docs/macros.py | 109 +++++++++++++++--- 2 files changed, 100 insertions(+), 25 deletions(-) diff --git a/docs/_templates/mkdocstrings/python/readthedocs/children.html.jinja b/docs/_templates/mkdocstrings/python/readthedocs/children.html.jinja index c60bae4..324e181 100644 --- a/docs/_templates/mkdocstrings/python/readthedocs/children.html.jinja +++ b/docs/_templates/mkdocstrings/python/readthedocs/children.html.jinja @@ -5,7 +5,7 @@ This template iterates on members of a given object and renders them. It can group members by category (attributes, classes, functions, modules) or render them in a flat list. Context: - obj (griffe.dataclasses.Object): The object to render. + obj (griffe.Object): The object to render. config (dict): The configuration options. root_members (bool): Whether the object is the root object. heading_level (int): The HTML heading level to use. @@ -31,7 +31,7 @@ Context: {%- endmacro -%} -{% if obj.members %} +{% if obj.all_members %} {% block logs scoped %} {#- Logging block. @@ -97,7 +97,7 @@ Context: filters=config.filters, members_list=members_list, inherited_members=config.inherited_members, - keep_no_docstrings=config.show_if_no_docstring)|selectattr("is_init_module", "equalto", false)|list|order_members(config.members_order, members_list) + keep_no_docstrings=config.show_if_no_docstring)|selectattr("is_init_module", "equalto", false)|list|order_members(config.members_order, members_list)|sort(attribute="name") %} {% if modules %} {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} @@ -127,7 +127,7 @@ Context: {% endif %} {% with heading_level = heading_level + extra_level %} {% for attribute in attributes|order_members(config.members_order, members_list) %} - {% if members_list is not none or attribute.is_public %} + {% if members_list is not none or (not attribute.is_imported or attribute.is_public) %} {% include attribute|get_template with context %} {% endif %} {% endfor %} @@ -147,7 +147,7 @@ Context: {% endif %} {% with heading_level = heading_level + extra_level %} {% for class in classes|order_members(config.members_order, members_list) %} - {% if members_list is not none or class.is_public %} + {% if members_list is not none or (not class.is_imported or class.is_public) %} {% include class|get_template with context %} {% endif %} {% endfor %} @@ -168,7 +168,7 @@ Context: {% with heading_level = heading_level + extra_level %} {% for function in functions|order_members(config.members_order, members_list) %} {% if not (obj.kind.value == "class" and function.name == "__init__" and config.merge_init_into_class) %} - {% if members_list is not none or function.is_public %} + {% if members_list is not none or (not function.is_imported or function.is_public) %} {% include function|get_template with context %} {% endif %} {% endif %} @@ -189,8 +189,8 @@ Context: {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} - {% for module in modules|order_members(config.members_order, members_list) %} - {% if members_list is not none or module.is_public %} + {% for module in modules|order_members(config.members_order.alphabetical, members_list) %} + {% if members_list is not none or (not module.is_alias or module.is_public) %} {% include module|get_template with context %} {% endif %} {% endfor %} diff --git a/docs/macros.py b/docs/macros.py index 1dd00f0..e63fcf5 100644 --- a/docs/macros.py +++ b/docs/macros.py @@ -1,5 +1,6 @@ """Macros for the documentation.""" +import abc import inspect import os import pathlib @@ -10,7 +11,7 @@ import tomli -from mkdocs_macros.plugin import MacrosPlugin # pyright: ignore[reportMissingTypeStubs] +from mkdocs_macros.plugin import MacrosPlugin HEADER_ONE_REGEX = re.compile(r"^#\s(.+)$", re.MULTILINE) PAGE_REPLACEMENTS = { @@ -23,11 +24,44 @@ FILES_TO_REMOVE_BLACK_FORMATTER_DISABLE_COMMENT = { "basic_usage.md", } +CONVERSION_PATTERN = re.compile( + r"> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER)]\s*>\s*(.*?)(?=\n[^>]|$)", + re.IGNORECASE | re.DOTALL, +) #################################################################################################### # Helper functions #################################################################################################### +def convert_gfm_alerts_to_admonitions(content: str) -> str: + """Convert GitHub Flavored Markdown (GFM) alerts to MkDocs admonitions. + + Args: + content: The content to convert. + + Returns: + The updated content with GFM alerts converted to markdown admonitions. + """ + + def replace_match(match: "re.Match[str]") -> str: + """Replace the matched GFM alert with an admonition. + + Args: + match: The matched GFM alert. + + Returns: + The replacement text. + """ + alert_type = match.group(1).lower() + text = match.group(2).strip() + # Replace initial '>' from subsequent lines + text = text.replace("\n>", "\n") + # Replace with admonition format + return f"!!! {alert_type}\n " + text.replace("\n", "\n ") + + return re.sub(CONVERSION_PATTERN, replace_match, content) + + def import_object(objname: str) -> Any: """Import a python object by its qualified name. @@ -90,11 +124,15 @@ def get_classes(*cls_or_modules: str, strict: bool = False) -> Generator[Any, No #################################################################################################### # Macro functions #################################################################################################### -def class_diagram( +def class_diagram( # noqa: C901 # pylint: disable=too-many-locals *cls_or_modules: str, full: bool = False, strict: bool = False, namespace: Optional[str] = None, + tree_direction: str = "up", + chart_direction: str = "LR", + highlight_family_base_classes: bool = False, + highlight_device_drivers: bool = False, ) -> str: """Create a mermaid classDiagram for the provided classes or modules. @@ -104,6 +142,13 @@ def class_diagram( strict: A boolean indicating to only consider classes that are strictly defined in that module and not imported from somewhere else. namespace: Limits the diagram to only include classes defined in this namespace. + tree_direction: A string indicating the direction of traversal in the class hierarchy, + either "up" or "down". + chart_direction: A string indicating the direction of the chart, either + "LR" (left to right), "RL" (right to left), + "TB" (top to bottom), or "BT" (bottom to top). + highlight_family_base_classes: Indicate to highlight the family base classes in cyan. + highlight_device_drivers: Indicate to highlight the device drivers in lightgreen. Returns: The mermaid code block with complete syntax for the classDiagram. @@ -112,8 +157,15 @@ def class_diagram( ValueError: If no classDiagram can be created. """ inheritances: Set[Tuple[str, str]] = set() + family_base_classes: Set[str] = set() + device_drivers: Set[str] = set() + + def get_tree_upwards(cls: Any) -> None: + if getattr(cls, "_product_family_base_class", None) == cls: + family_base_classes.add(cls.__name__) + if abc.ABC not in cls.__bases__: + device_drivers.add(cls.__name__) - def get_tree(cls: Any) -> None: for base in cls.__bases__: if base.__name__ == "object": continue @@ -121,7 +173,22 @@ def get_tree(cls: Any) -> None: continue inheritances.add((base.__name__, cls.__name__)) if full: - get_tree(base) + get_tree_upwards(base) + + def get_tree_downwards(cls: Any) -> None: + if getattr(cls, "_product_family_base_class", None) == cls: + family_base_classes.add(cls.__name__) + if abc.ABC not in cls.__bases__: + device_drivers.add(cls.__name__) + + for subclass in cls.__subclasses__(): + if namespace and not subclass.__module__.startswith(namespace): + continue + inheritances.add((cls.__name__, subclass.__name__)) + if full: + get_tree_downwards(subclass) + + get_tree = get_tree_upwards if tree_direction == "up" else get_tree_downwards for cls_item in get_classes(*cls_or_modules, strict=strict): get_tree(cls_item) @@ -130,11 +197,18 @@ def get_tree(cls: Any) -> None: msg = "No class hierarchy can be created." raise ValueError(msg) - return ( - "```mermaid\nclassDiagram\n" - + "\n".join(f" {a} <|-- {b}" for a, b in sorted(inheritances)) - + "\n```" + mermaid_code_block = f"```mermaid\nclassDiagram\n direction {chart_direction}\n" + "\n".join( + f" {a} <|-- {b}" for a, b in sorted(inheritances) ) + if highlight_family_base_classes: + for family_base_class in sorted(family_base_classes): + mermaid_code_block += f"\n style {family_base_class} stroke:orangered,stroke-width:4px" + if highlight_device_drivers: + for device_driver in sorted(device_drivers): + mermaid_code_block += f"\n style {device_driver} fill:lightgreen" + mermaid_code_block += "\n```" + + return mermaid_code_block def create_repo_link(link_text: str, base_repo_url: str, relative_repo_path: str) -> str: @@ -162,8 +236,7 @@ def define_env(env: MacrosPlugin) -> None: """ # Read in the current package version number to use in templates and files with open( - pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml", - "rb", + pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml", "rb" ) as file_handle: pyproject_data = tomli.load(file_handle) package_version = "v" + pyproject_data["tool"]["poetry"]["version"] @@ -183,14 +256,16 @@ def define_env(env: MacrosPlugin) -> None: def on_post_page_macros(env: MacrosPlugin) -> None: """Post-process pages.""" # Check if there are any replacements to perform on the page - if env.page.file.src_path in PAGE_REPLACEMENTS: # pyright: ignore[reportUnknownMemberType] - for search, replace in PAGE_REPLACEMENTS[env.page.file.src_path]: # pyright: ignore[reportUnknownMemberType] - env.markdown = env.markdown.replace(search, replace) # pyright: ignore[reportUnknownMemberType] + if env.page.file.src_path in PAGE_REPLACEMENTS: + for search, replace in PAGE_REPLACEMENTS[env.page.file.src_path]: + env.markdown = env.markdown.replace(search, replace) # Check if all black format disable comments should be removed from the page - if env.page.file.src_path in FILES_TO_REMOVE_BLACK_FORMATTER_DISABLE_COMMENT: # pyright: ignore[reportUnknownMemberType] - env.markdown = env.markdown.replace("# fmt: off\n", "") # pyright: ignore[reportUnknownMemberType] + if env.page.file.src_path in FILES_TO_REMOVE_BLACK_FORMATTER_DISABLE_COMMENT: + env.markdown = env.markdown.replace("# fmt: off\n", "") + # Check if there are any admonitions to replace on the page + env.markdown = convert_gfm_alerts_to_admonitions(env.markdown) # Check if the title is correct - if actual_title_match := HEADER_ONE_REGEX.search(env.markdown): # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] + if actual_title_match := HEADER_ONE_REGEX.search(env.markdown): actual_title = actual_title_match.group(1) if env.page.title != actual_title: # pyright: ignore[reportUnknownMemberType] - env.page.title = actual_title # pyright: ignore[reportUnknownMemberType] + env.page.title = actual_title # pyright: ignore[reportAttributeAccessIssue]