Skip to content

Commit

Permalink
docs: Update documentation templates and macros (#38)
Browse files Browse the repository at this point in the history
* docs: Update documentation templates and macros

* ci: Remove broken hook
  • Loading branch information
nfelt14 authored Oct 22, 2024
1 parent 094f3f6 commit 84050b3
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 31 deletions.
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ repos:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/PyCQA/docformatter
rev: dfefe062799848234b4cd60b04aa633c0608025e # frozen: v1.7.5
hooks:
- id: docformatter
additional_dependencies: [tomli]
# TODO: Re-enable this once https://github.com/PyCQA/docformatter/issues/293 is resolved
# - repo: https://github.com/PyCQA/docformatter
# rev: dfefe062799848234b4cd60b04aa633c0608025e # frozen: v1.7.5
# hooks:
# - id: docformatter
# additional_dependencies: [tomli]
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -31,7 +31,7 @@ Context:
</table>
{%- endmacro -%}

{% if obj.members %}
{% if obj.all_members %}
{% block logs scoped %}
{#- Logging block.
Expand Down Expand Up @@ -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 %}
Expand Down Expand Up @@ -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 %}
Expand All @@ -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 %}
Expand All @@ -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 %}
Expand All @@ -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 %}
Expand Down
109 changes: 92 additions & 17 deletions docs/macros.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Macros for the documentation."""

import abc
import inspect
import os
import pathlib
Expand All @@ -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 = {
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -112,16 +157,38 @@ 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
if namespace and not base.__module__.startswith(namespace):
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)
Expand All @@ -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:
Expand Down Expand Up @@ -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"]
Expand All @@ -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]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ numpy = [
{python = "^3.8, <3.12", version = "^1.24"}
]
protobuf = "^5.28.1"
python = "^3.8"
python = "^3.8, <3.13"
python-dateutil = "^2.8.2"
tm_data_types = "^0.1.0"
typing_extensions = "^4.12.0"
Expand Down

0 comments on commit 84050b3

Please sign in to comment.