-
Notifications
You must be signed in to change notification settings - Fork 0
Add a directives for general purpose documentation in downstream proj… #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
from importlib.metadata import version | ||
from pathlib import Path | ||
|
||
from django.template import Template | ||
from django.template.context import Context | ||
from django.utils.module_loading import import_string | ||
|
||
from docutils import nodes | ||
from docutils.parsers.rst import Directive, directives | ||
from docutils.statemachine import ViewList | ||
from pydantic.dataclasses import dataclass | ||
|
||
from django_setup_configuration.configuration import BaseConfigurationStep | ||
|
||
_TEMPLATES_PATH = Path(__file__).parent / "templates" | ||
|
||
|
||
def _parse_bool(argument): | ||
value = directives.choice(argument, ("true", "false", "yes", "no", "1", "0")) | ||
return value in ("true", "yes", "1") | ||
|
||
|
||
@dataclass(frozen=True) | ||
class StepInfo: | ||
title: str | ||
description: str | ||
anchor_id: str | ||
module_path: str | ||
step_cls: type[BaseConfigurationStep] | ||
|
||
|
||
class SetupConfigUsageDirective(Directive): | ||
has_content = True | ||
|
||
option_spec = { | ||
"show_command_usage": _parse_bool, | ||
"show_steps": _parse_bool, | ||
"show_steps_toc": _parse_bool, | ||
"show_steps_autodoc": _parse_bool, | ||
} | ||
|
||
def run(self): | ||
show_command_usage = self.options.get("show_command_usage", True) | ||
show_steps = self.options.get("show_steps", True) | ||
show_steps_toc = self.options.get("show_steps_toc", True) | ||
show_steps_autodoc = self.options.get("show_steps_autodoc", True) | ||
|
||
if not (settings := self._get_django_settings()): | ||
raise ValueError( | ||
"Unable to load Django settings. Is DJANGO_SETTINGS_MODULE set?" | ||
) | ||
|
||
if not (configured_steps := settings.get("SETUP_CONFIGURATION_STEPS")): | ||
raise ValueError( | ||
"No steps configured. Set SETUP_CONFIGURATION_STEPS via your " | ||
"Django settings." | ||
) | ||
|
||
usage_template = self._load_usage_template() | ||
steps = self._load_steps(configured_steps) | ||
|
||
rst = ViewList() | ||
rst.append("", "<dynamic>") | ||
usage_rst = usage_template.render( | ||
context=Context( | ||
{ | ||
"steps": steps, | ||
"show_toc": show_steps_toc, | ||
"show_steps": show_steps, | ||
"package_version": version("django_setup_configuration"), | ||
} | ||
) | ||
) | ||
lines = usage_rst.split("\n") | ||
for line in lines: | ||
rst.append(line, "<dynamic>") | ||
|
||
usage_node: nodes.section | None = None | ||
if show_command_usage: | ||
usage_node = nodes.section() | ||
usage_node["ids"] = ["django-setup-config"] | ||
usage_node += nodes.title( | ||
text="Using the setup_configuration management command" | ||
) | ||
self.state.nested_parse(rst, 0, usage_node) | ||
|
||
step_sections: list[nodes.section] = [] | ||
if show_steps: | ||
for step in steps: | ||
step_node = nodes.section(ids=[step.module_path]) | ||
step_node += nodes.title(text=step.title) | ||
|
||
rst = ViewList() | ||
rst.append(f".. _{step.anchor_id}:", "<dynamic>") | ||
if show_steps_autodoc: | ||
rst.append(f".. autoclass:: {step.module_path}", "<dynamic>") | ||
rst.append(" :noindex:", "<dynamic>") | ||
else: | ||
# Explicitly display the docstring if there's no autodoc to serve | ||
# as the step description. | ||
for line in step.description.splitlines(): | ||
Coperh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
rst.append(line, "<dynamic>") | ||
|
||
rst.append(f".. setup-config-example:: {step.module_path}", "<dynamic>") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @swrichards something else I ran into just now: it might be useful to add anchors to the steps, that way you can still reference specific steps from other pages
not entirely sure what the name of the reference should be though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have now rewritten this so the step sections are always RST sections, which mean they will always have an anchor id, which I've changed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the only way to cross-reference is by adding an explicit reference like I tried linking to this another way, the only thing that seems to work is spelling out the path of the file + the anchor (https://github.com/open-zaak/open-notificaties/pull/228/files#diff-056134d467e07b8255b1205455315b00fd959de8fe75ce378dc3b0698587c7a8L82), but that reference isn't validated when building the docs, so that link would break if the path/anchor change and you wouldn't be notified about it by a failing build |
||
|
||
self.state.nested_parse(rst, 0, step_node) | ||
step_sections.append(step_node) | ||
|
||
return [node for node in (usage_node, *step_sections) if node] | ||
|
||
@classmethod | ||
def _get_django_settings(cls): | ||
from django.conf import settings | ||
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured | ||
|
||
try: | ||
return settings._wrapped.__dict__ if hasattr(settings, "_wrapped") else {} | ||
except (AppRegistryNotReady, AttributeError, ImproperlyConfigured): | ||
return {} | ||
|
||
def _load_usage_template(self): | ||
return Template((_TEMPLATES_PATH / "config_doc.rst").read_text()) | ||
|
||
def _load_steps(self, configured_steps) -> list[StepInfo]: # -> list: | ||
steps_info: list[StepInfo] = [] | ||
for step_path in configured_steps: | ||
step_cls = import_string(step_path) | ||
step_info = StepInfo( | ||
title=step_cls.verbose_name, | ||
anchor_id=f"ref_step_{step_path}", | ||
module_path=step_path, | ||
step_cls=step_cls, | ||
description=step_cls.__doc__ or "", | ||
) | ||
steps_info.append(step_info) | ||
return steps_info | ||
|
||
|
||
def setup(app): | ||
app.add_directive("setup-config-usage", SetupConfigUsageDirective) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
You can use the included ``setup_configuration`` management command to configure your | ||
instance from a yaml file as follows: | ||
|
||
.. code-block:: bash | ||
|
||
python manage.py setup_configuration --yaml-file /path/to/config.yaml | ||
|
||
You can also validate that the configuration source can be successfully loaded, | ||
without actually running the steps, by adding the ``validate-only`` flag: | ||
|
||
.. code-block:: bash | ||
|
||
python manage.py setup_configuration --yaml-file /path/to/config.yaml --validate-only | ||
|
||
Both commands will either return 0 and a success message if the configuration file can | ||
be loaded without issues, otherwise it will return a non-zero exit code and print any | ||
validation errors. | ||
|
||
Your YAML file should contain both a flag indicating whether the step is enabled or | ||
disabled, as well as an object containing the actual configuration values under the | ||
appropriate key. | ||
|
||
.. note:: All steps are disabled by default. You only have to explicitly include the | ||
flag to enable a step, not to disable it, though you may do so if you wish to | ||
have an explicit record of what steps are disabled. | ||
|
||
Further information can be found at the `django-setup-configuration | ||
<https://django-setup-configuration.readthedocs.io/en/{{ package_version }}/quickstart.html#command-usage>`_ documentation. | ||
|
||
{% if show_toc %} | ||
|
||
{% if show_steps %} | ||
This projects includes the following configuration steps (click on each step for a | ||
brief descripion and an example YAML you can include in your config file): | ||
{% else %} | ||
This projects includes the following configuration steps: | ||
{% endif %} | ||
|
||
{% for step in steps %} | ||
- {% if show_steps %}`{{ step.title }} <#{{ step.anchor_id }}>`_{% else %} {{ step.title }} {% endif %} | ||
{% endfor %} | ||
{% endif %} |
This file was deleted.
swrichards marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,104 @@ | ||
.. _config_docs: | ||
|
||
Configuration documentation | ||
Configuration Documentation | ||
=========================== | ||
|
||
The library provides a Sphinx directive that generates (and validates) an example configuration | ||
file in YAML format for a given ``ConfigurationStep``, containing information about the names of the fields, | ||
possible values, default values, and a short description. This helps clients determine | ||
what can be configured with the help of the library and how. | ||
The library provides two Sphinx directives: | ||
|
||
1. ``setup-config-example`` - Generates (and validates) an example configuration file in YAML format for a given ``ConfigurationStep``. This includes information about field names, possible values, default values, and descriptions, helping clients understand available configuration options. | ||
|
||
Setup | ||
""""" | ||
2. ``setup-config-usage`` - Generates basic usage information and lists all configured steps with metadata and example YAMLs (it does this by wrapping ``setup-config-example`` so the examples will be validated as well). This provides a complete overview for users who want to bootstrap their installation. | ||
|
||
Start by adding the following extension to ``conf.py`` in the documentation directory: | ||
Using setup-config-example | ||
-------------------------- | ||
|
||
:: | ||
First, add the extension and its requirements to ``conf.py`` in your documentation directory: | ||
|
||
.. code-block:: python | ||
|
||
extensions = [ | ||
... | ||
"sphinx.ext.autodoc", | ||
stevenbal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"django_setup_configuration.documentation.setup_config_example", | ||
... | ||
] | ||
|
||
And then display a YAML example by using the directive: | ||
|
||
:: | ||
Then display a YAML example using the directive: | ||
|
||
.. code-block:: rst | ||
|
||
.. setup-config-example:: path.to.your.ConfigurationStep | ||
|
||
which will produce something like the following example (in the case of the ``SitesConfigurationStep`` provided by this library): | ||
This will produce output similar to the following example (using the ``SitesConfigurationStep`` provided by this library): | ||
|
||
.. setup-config-example:: django_setup_configuration.contrib.sites.steps.SitesConfigurationStep | ||
|
||
.. warning:: | ||
|
||
Not all possible configurations are supported by this directive currently. | ||
More complex type annotations like ``list[ComplexObject | ComplexObject]`` will raise errors when | ||
trying to build the documentation | ||
Not all configurations are currently supported by this directive. | ||
Complex type annotations like ``list[ComplexObject | ComplexObject]`` will raise errors during documentation build. | ||
|
||
Using setup-config-usage | ||
------------------------ | ||
|
||
First, add the extension and its requirements to ``conf.py`` in your documentation directory: | ||
|
||
.. code-block:: python | ||
|
||
extensions = [ | ||
... | ||
"sphinx.ext.autodoc", | ||
"django_setup_configuration.documentation.setup_config_example", | ||
"django_setup_configuration.documentation.setup_config_usage", | ||
... | ||
] | ||
|
||
To use this directive, you'll also have to ensure Django is configured and initialized | ||
in your Sphinx `conf.py` file, for instance like this: | ||
|
||
.. code-block:: python | ||
|
||
# docs/conf.py | ||
|
||
# ... | ||
import django | ||
from django.conf import settings | ||
|
||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_settings_module") | ||
django.setup() | ||
|
||
# ... | ||
# extensions = [...] | ||
|
||
Then display usage information using the directive: | ||
|
||
.. code-block:: rst | ||
|
||
.. setup-config-usage:: | ||
|
||
|
||
This generates a "how to" introduction for invoking the management command, | ||
followed by sections for each configured step with example YAML configurations. | ||
|
||
By default, the directive will output a full documentation page, but you can hide individual | ||
sections using the following options: | ||
|
||
- ``show_command_usage``: whether to include basic usage information on how to invoke the management command | ||
- ``show_steps``: whether to display information about the configured steps | ||
- ``show_steps_toc``: whether to include a short table of contents of all configured steps, before displaying the individual step sections | ||
- ``show_steps_autodoc``: whether to include an ``autodoc`` section showing the full path to the step module | ||
|
||
For example, to hide the usage section, show the steps without autodoc: | ||
|
||
.. code-block:: rst | ||
|
||
.. setup-config-usage:: | ||
:show_command_usage: false | ||
:show_steps_autodoc: false | ||
|
||
|
||
.. note:: | ||
|
||
The titles for the step sections will be taken from the step's ``verbose_title`` field, | ||
whereas the descriptions are taken from the step class's docstring (if present). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"subdirectory": "fixtures" | ||
} |
Uh oh!
There was an error while loading. Please reload this page.