Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a directives for general purpose documentation in downstream proj…
Browse files Browse the repository at this point in the history
…ects
swrichards committed Jan 30, 2025

Unverified

This user has not yet uploaded their public signing key.
1 parent 24e287e commit 2b8f56a
Showing 16 changed files with 606 additions and 75 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include *.rst
include LICENSE
include django_setup_configuration/py.typed
include django_setup_configuration/documentation/templates/*.rst
recursive-include django_setup_configuration *.html
recursive-include django_setup_configuration *.txt
recursive-include django_setup_configuration *.po
137 changes: 137 additions & 0 deletions django_setup_configuration/documentation/setup_config_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from dataclasses import dataclass
from pathlib import Path

from django.template import Template
from django.template.context import Context
from django.utils.module_loading import import_string
from django.utils.text import slugify

from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import ViewList

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: 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,
}
)
)
lines = usage_rst.split("\n")
for line in lines:
rst.append(line, "<dynamic>")

root_node = nodes.container()
if show_command_usage:
root_node = nodes.section()
root_node["ids"] = ["django-setup-config"]
root_node += nodes.title(
text="Using the setup_configuration management command"
)
self.state.nested_parse(rst, 0, root_node)

if show_steps:
for step in steps:
rst = ViewList()
step_node = nodes.section(ids=[step.anchor_id])
step_node += nodes.title(text=step.title)

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():
rst.append(line, "<dynamic>")

rst.append(f".. setup-config-example:: {step.module_path}", "<dynamic>")

self.state.nested_parse(rst, 0, step_node)
root_node += step_node

return [root_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=slugify(step_cls.verbose_name),
module_path=step_path,
step_cls=step_cls,
description=step_cls.__doc__,
)
steps_info.append(step_info)
return steps_info


def setup(app):
app.add_directive("setup-config-usage", SetupConfigUsageDirective)
42 changes: 42 additions & 0 deletions django_setup_configuration/documentation/templates/config_doc.rst
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://github.com/maykinmedia/django-setup-configuration/>`_ project page.

{% 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.

1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -56,6 +56,7 @@
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"django_setup_configuration.documentation.setup_config_example",
"django_setup_configuration.documentation.setup_config_usage",
]

# Add any paths that contain templates here, relative to this directory.
76 changes: 61 additions & 15 deletions docs/config_docs.rst
Original file line number Diff line number Diff line change
@@ -1,39 +1,85 @@
.. _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. 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",
"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 expected YAML configurations.

.. note::

For clarity, create a separate RST file (e.g., ``setup-configuration.rst``) containing only this directive.
Sphinx will include the subsections for each step in your documentation's navigation area.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -52,7 +52,8 @@ tests = [
"isort",
"black",
"flake8",
"python-decouple"
"python-decouple",
"sphinx"
]
coverage = [
"pytest-cov",
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
from pathlib import Path
from unittest import mock

from django.contrib.auth.models import User
@@ -12,6 +13,13 @@
from django_setup_configuration.runner import SetupConfigurationRunner
from testapp.configuration import BaseConfigurationStep

pytest_plugins = ("sphinx.testing.fixtures",)


@pytest.fixture(scope="session")
def rootdir() -> Path:
return Path(__file__).resolve().parent / "sphinx-roots"


@pytest.fixture
def yaml_file_factory(tmp_path_factory):
Loading

0 comments on commit 2b8f56a

Please sign in to comment.