Deployed f54f3cc to dev with MkDocs 1.6.1 and mike 2.1.3

This commit is contained in:
github-actions 2025-03-19 11:56:01 +00:00
parent 5933e103bc
commit f0276a55e7
146 changed files with 6953 additions and 454 deletions

View file

@ -42,14 +42,15 @@ from argparse import ArgumentParser
from importlib import import_module
from pathlib import Path
from textwrap import dedent
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Type, Union
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Type, Union
from django.core.management.base import BaseCommand
from django.urls import URLPattern, URLResolver
from django_components import ComponentVars, TagFormatterABC
from django_components.component import Component
from django_components import Component, ComponentVars, ComponentCommand, TagFormatterABC
from django_components.commands.components import ComponentsRootCommand
from django_components.node import BaseNode
from django_components.util.command import setup_parser_from_command
from django_components.util.misc import get_import_path
# NOTE: This file is an entrypoint for the `gen-files` plugin in `mkdocs.yml`.
@ -108,6 +109,37 @@ def gen_reference_api():
f.write("\n")
def gen_reference_testing_api():
"""
Generate documentation for the Python API of `django_components.testing`.
This takes all public symbols exported from `django_components.testing`.
"""
module = import_module("django_components.testing")
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_testing_api.md").read_text()
out_file = root / "docs/reference/testing_api.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
for name, obj in inspect.getmembers(module):
if name.startswith("_") or inspect.ismodule(obj):
continue
# For each entry, generate a mkdocstrings entry, e.g.
# ```
# ::: django_components.testing.djc_test
# options:
# show_if_no_docstring: true
# ```
f.write(f"::: {module.__name__}.{name}\n" f" options:\n" f" show_if_no_docstring: true\n")
f.write("\n")
def gen_reference_exceptions():
"""
Generate documentation for the Exception classes included in the Python API of `django_components`.
@ -431,13 +463,6 @@ def gen_reference_commands():
These are discovered by looking at the files defined inside `management/commands`.
"""
command_files = Path("./src/django_components/management/commands").glob("*.py")
command_modules = [
(p.stem, f"django_components.management.commands.{p.stem}")
for p in command_files
if not p.stem.startswith("_")
]
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_commands.md").read_text()
out_file = root / "docs/reference/commands.md"
@ -446,13 +471,72 @@ def gen_reference_commands():
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
for cmd_name, cmd_path in command_modules:
cmd_module = import_module(cmd_path)
# Document all commands defined by django-components
# All our commands are scoped under `components` (e.g. `components create`, `components upgrade`, etc.)
# Furthermore, all subcommands are declared statically, so we can walk down the tree of subcommands.
commands_stack: List[Tuple[Type[ComponentCommand], Tuple[str, ...]]] = [(ComponentsRootCommand, ())]
while commands_stack:
cmd_def_cls, cmd_path = commands_stack.pop()
# NOTE: Argparse formats the help string, and so it uses `%%` to escape `%` characters.
# So we need to replace them with `%`
cmd_summary = cmd_def_cls.help.replace("%%", "%") if cmd_def_cls.help else ""
cmd_desc = dedent(cmd_def_cls.__doc__ or "")
cmd_name = " ".join(cmd_path) + " " + cmd_def_cls.name
cmd_parser = setup_parser_from_command(cmd_def_cls)
cmd_usage = cmd_parser.format_usage()
# NOTE: The generated usage shows only the command name, not the full path.
# So we need to add it manually.
#
# So this:
# `usage: ext run [-h]`
#
# becomes this:
# `usage: python manage.py components ext run [-h]`
cmd_usage = cmd_usage[:7] + "python manage.py " + " ".join(cmd_path) + " " + cmd_usage[7:]
formatted_args = _format_command_args(cmd_parser, cmd_path + (cmd_def_cls.name,))
# Add link to source code
module_abs_path = import_module(cmd_def_cls.__module__).__file__
module_rel_path = Path(module_abs_path).relative_to(Path.cwd()).as_posix() # type: ignore[arg-type]
obj_lineno = inspect.findsource(cmd_def_cls)[1]
source_code_link = _format_source_code_html(module_rel_path, obj_lineno)
# NOTE: For the commands we have to generate the markdown entries ourselves,
# instead of delegating to mkdocs, for two reasons:
# 1. All commands have to use the class name `Command` for Django to pick them up
# 2. The command name is actually defined by the file name.
f.write(
f"## `{cmd_name}`\n\n"
f"```txt\n{cmd_usage}\n```\n\n"
f"{source_code_link}\n\n"
f"{cmd_summary}\n\n"
f"{formatted_args}\n\n"
f"{cmd_desc}\n\n"
)
# Add subcommands
for subcmd_cls in reversed(cmd_def_cls.subcommands):
commands_stack.append((subcmd_cls, cmd_path + (cmd_def_cls.name,)))
# TODO_v1 - REMOVE - This this section as it only for legacy commands `startcomponent` and `upgradecomponent`
command_files = Path("./src/django_components/management/commands").glob("*.py")
command_modules = [
(p.stem, f"django_components.management.commands.{p.stem}")
for p in command_files
if not p.stem.startswith("_")
]
for cmd_name, cmd_import_path in command_modules:
# NOTE: `components` command is already documented in the non-legacy section
if cmd_name == "components":
continue
cmd_module = import_module(cmd_import_path)
cmd_cls: BaseCommand = cmd_module.Command
cmd_summary = cmd_cls.help
cmd_desc = dedent(cmd_cls.__doc__ or "")
cmd_parser: ArgumentParser = cmd_cls().create_parser("manage.py", cmd_name)
cmd_usage: str = cmd_parser.format_usage()
cmd_parser = cmd_cls().create_parser("manage.py", cmd_name)
cmd_usage = cmd_parser.format_usage()
formatted_args = _format_command_args(cmd_parser)
# Add link to source code
@ -552,6 +636,178 @@ def gen_reference_templatevars():
f.write(f"::: {ComponentVars.__module__}.{ComponentVars.__name__}.{field}\n\n")
def gen_reference_extension_hooks():
"""
Generate documentation for the hooks that are available to the extensions.
"""
module = import_module("django_components.extension")
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_extension_hooks.md").read_text()
out_file = root / "docs/reference/extension_hooks.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
with out_file.open("w", encoding="utf-8") as f:
# 1. Insert section from `reference_extension_hooks.md`
f.write(preface + "\n\n")
# 2. Print each hook and their descriptions
extension_cls = module.ComponentExtension
class_name = get_import_path(extension_cls)
# NOTE: If no unique methods, just document the class itself without methods
unique_methods = _get_unique_methods(NamedTuple, extension_cls)
# All hooks start with `on_`, so filter out the rest
unique_methods = [name for name in unique_methods if name.startswith("on_")]
for name in sorted(unique_methods):
# Programmatically get the data available inside the hook, so we can generate
# a table of available data.
# Each hook receives a second "ctx" argument, so we access the typing of this "ctx",
# and get its fields.
method = getattr(extension_cls, name)
ctx_type = method.__annotations__["ctx"]
# The Context data class is defined in the same module as the hook, so we can
# import it dynamically.
ctx_class = getattr(module, ctx_type.__name__)
fields = ctx_class._fields
field_docstrings = _extract_property_docstrings(ctx_class)
# Generate the available data table
available_data = "**Available data:**\n\n"
available_data += "name | type | description\n"
available_data += "--|--|--\n"
for field in sorted(fields):
field_type = _format_hook_type(str(ctx_class.__annotations__[field]))
field_desc = field_docstrings[field]
available_data += f"`{field}` | {field_type} | {field_desc}\n"
# For each entry, generate a mkdocstrings entry, e.g.
# ```
# ::: django_components.extension.ComponentExtension.on_component_registered
# options:
# ...
# ```
f.write(
f"::: {class_name}.{name}\n"
f" options:\n"
f" show_root_heading: true\n"
f" show_signature: true\n"
f" separate_signature: true\n"
f" show_symbol_type_heading: false\n"
f" show_symbol_type_toc: false\n"
f" show_if_no_docstring: true\n"
f" show_labels: false\n"
)
f.write("\n")
f.write(available_data)
f.write("\n")
forward_ref_pattern = re.compile(r"ForwardRef\('(.+?)'\)")
class_repr_pattern = re.compile(r"<class '(.+?)'>")
typing_pattern = re.compile(r"typing\.(.+?)")
def _format_hook_type(type_str: str) -> str:
# Clean up the type string
type_str = forward_ref_pattern.sub(r"\1", type_str)
type_str = class_repr_pattern.sub(r"\1", type_str)
type_str = typing_pattern.sub(r"\1", type_str)
type_str = type_str.replace("django.template.context.Context", "Context")
type_str = "`" + type_str + "`"
# Add links to non-builtin types
if "ComponentRegistry" in type_str:
type_str = f"[{type_str}](../api#django_components.ComponentRegistry)"
elif "Component" in type_str:
type_str = f"[{type_str}](../api#django_components.Component)"
elif "Context" in type_str:
type_str = f"[{type_str}](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)"
return type_str
def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
"""
Python doesn't provide a way to access docstrings of properties, e.g.:
```python
class MyComponent(Component):
my_property: str = "Hello, world!"
'''
My property docstring
'''
```
This function extracts the docstrings of properties from the source code.
Returns a dictionary with the property name as the key and the docstring as the value.
```python
{
"my_property": "My property docstring"
}
```
NOTE: This is a naive implementation and may not work for all cases:
- The function expects NO colons (`:`) inside class bases definition.
- The function assumes that the docstring is defined with `\"\"\"` or `'''`, and that
the docstring begins on a separate line.
- The function assumes that the class is defined at the global scope (module level)
and that the body is indented with 4 spaces.
"""
lines, start_line_index = inspect.getsourcelines(cls)
attrs_lines = []
ignore = True
for line in lines:
if ignore:
if line.endswith("):\n"):
ignore = False
continue
else:
attrs_lines.append(line)
attrs_docstrings = {}
curr_attr = None
docstring_delimiter = None
state = "before_attr"
while attrs_lines:
line = attrs_lines.pop(0)
# Exactly 1 indentation and not empty line
is_one_indent = line.startswith(" " * 4) and not line.startswith(" " * 5) and line.strip()
line = line.strip()
if state == "before_attr":
if not is_one_indent:
continue
curr_attr = line.split(":", maxsplit=1)[0].strip()
attrs_docstrings[curr_attr] = ""
state = "before_attr_docstring"
elif state == "before_attr_docstring":
if not is_one_indent or not (line.startswith("'''") or line.startswith('"""')):
continue
# Found start of docstring
docstring_delimiter = line[0:3]
line = line[3:]
attrs_lines.insert(0, line)
state = "attr_docstring"
elif state == "attr_docstring":
# Not end of docstring
if docstring_delimiter not in line: # type: ignore[operator]
attrs_docstrings[curr_attr] += line # type: ignore[index]
continue
# Found end of docstring
last_docstring_line, _ = line.split(docstring_delimiter, maxsplit=1)
attrs_docstrings[curr_attr] += last_docstring_line # type: ignore[index]
attrs_docstrings[curr_attr] = dedent(attrs_docstrings[curr_attr]) # type: ignore[index]
state = "before_attr"
return attrs_docstrings
# NOTE: Unlike other references, the API of Signals is not yet codified (AKA source of truth defined
# as Python code). Instead, we manually list all signals that are sent by django-components.
def gen_reference_signals():
@ -647,6 +903,36 @@ def _gen_command_args(parser: ArgumentParser) -> str:
# {'desc': "Show program's version number and exit.",
# ```
def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
# Replace
# ```
# subcommands:
# {create,upgrade,ext}
# create Create a new django component.
# upgrade Upgrade django components syntax from ...
# ext Run extension commands.
# ```
#
# Into:
# ```
# subcommands:
# create Create a new django component.
# upgrade Upgrade django components syntax from ...
# ext Run extension commands.
# ```
if "subcommands:" in cmd_inputs:
cmd_inputs = re.compile(r"subcommands:\n.*?\}", re.DOTALL).sub("subcommands:", cmd_inputs)
# Dedent the lines that contain subcommands from 4 spaces to 2 spaces
text_before_subcommands, text_after_subcommands = cmd_inputs.split("subcommands:\n")
lines_after_subcommands = text_after_subcommands.split("\n")
new_text_after_subcommands = ""
for line in lines_after_subcommands:
if line.startswith(" " * 4):
new_text_after_subcommands += line[2:] + "\n"
else:
new_text_after_subcommands += line + "\n"
cmd_inputs = text_before_subcommands + "subcommands:\n" + new_text_after_subcommands
section: Optional[str] = None
data: Dict[str, List[Dict]] = {}
@ -690,7 +976,7 @@ def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
return data
def _format_command_args(cmd_parser: ArgumentParser):
def _format_command_args(cmd_parser: ArgumentParser, cmd_path: Optional[Sequence[str]] = None):
cmd_inputs: str = _gen_command_args(cmd_parser)
parsed_cmd_inputs = _parse_command_args(cmd_inputs)
@ -698,9 +984,15 @@ def _format_command_args(cmd_parser: ArgumentParser):
for section, args in parsed_cmd_inputs.items():
formatted_args += f"**{section.title()}:**\n\n"
for arg in args:
formatted_args += (
"- " + ", ".join([f"`{name}`" for name in arg["names"]]) + f"\n - {arg['desc']}" + "\n"
)
# Add link to the subcommand
if section == "subcommands":
name = "`" + arg["names"][0] + "`"
if cmd_path:
name = "[" + name + "](../commands#" + "-".join(cmd_path) + "-" + name + ")"
else:
name = ", ".join([f"`{name}`" for name in arg["names"]])
formatted_args += "- " + name + f"\n - {arg['desc']}" + "\n"
formatted_args += "\n"
return formatted_args
@ -739,6 +1031,8 @@ def gen_reference():
gen_reference_templatetags()
gen_reference_templatevars()
gen_reference_signals()
gen_reference_testing_api()
gen_reference_extension_hooks()
# This is run when `gen-files` plugin is run in mkdocs.yml