Skip to content

Commit

Permalink
Add a directives for general purpose documentation in downstream proj…
Browse files Browse the repository at this point in the history
…ects
  • Loading branch information
swrichards committed Jan 30, 2025
1 parent 24e287e commit 238f5e2
Show file tree
Hide file tree
Showing 16 changed files with 605 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
Expand Down
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
Expand Up @@ -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.
Expand Down
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
Expand Up @@ -52,7 +52,8 @@ tests = [
"isort",
"black",
"flake8",
"python-decouple"
"python-decouple",
"sphinx"
]
coverage = [
"pytest-cov",
Expand Down
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
Expand All @@ -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):
Expand Down
Loading

0 comments on commit 238f5e2

Please sign in to comment.