mirror of
https://github.com/django-components/django-components.git
synced 2025-09-20 04:39:45 +00:00
Deployed f54f3cc
to dev with MkDocs 1.6.1 and mike 2.1.3
This commit is contained in:
parent
5933e103bc
commit
f0276a55e7
146 changed files with 6953 additions and 454 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue