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 29, 2025
1 parent 24e287e commit 40f9e8f
Show file tree
Hide file tree
Showing 12 changed files with 371 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
103 changes: 103 additions & 0 deletions django_setup_configuration/documentation/setup_config_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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
from docutils.statemachine import ViewList

from django_setup_configuration.configuration import BaseConfigurationStep

_TEMPLATES_PATH = Path(__file__).parent / "templates"


@dataclass(frozen=True)
class StepInfo:
title: str
anchor_id: str
module_path: str
step_cls: BaseConfigurationStep


class SetupConfigUsageDirective(Directive):
has_content = True

@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 run(self):
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 in your "
" via your Django settings."
)

_usage_template = Template((_TEMPLATES_PATH / "config_doc.rst").read_text())
_step_template = Template((_TEMPLATES_PATH / "config_step.rst").read_text())

steps_info = []
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,
)
steps_info.append(step_info)

usage_rendered = _usage_template.render(context=Context({"steps": steps_info}))

rst = ViewList()
rst.append("", "<dynamic>")

lines = usage_rendered.split("\n")
for line in lines:
rst.append(line, "<dynamic>")

section_node = nodes.section()
section_node["ids"] = ["django-setup-config"]
section_node += nodes.title(
text="Setting up your application with django-setup-config"
)
self.state.nested_parse(rst, 0, section_node)

for step in steps_info:
rst = ViewList()
subsection_node = nodes.section(ids=[step.anchor_id])
subsection_node += nodes.title(text=step.title)

text = _step_template.render(context=Context({"step": step}))
lines = text.split("\n")

rst.append("", "<dynamic>") # Blank line
for line in lines:
rst.append(line, "<dynamic>")

self.state.nested_parse(rst, 0, subsection_node)
section_node += subsection_node

# self.state.nested_parse(rst, 0, section_node)
return [section_node]


def setup(app):
app.add_directive("setup-config-usage", SetupConfigUsageDirective)

return {"version": "1.0", "parallel_read_safe": True}
35 changes: 35 additions & 0 deletions django_setup_configuration/documentation/templates/config_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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.

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):

{% for step in steps %}
- `{{ step.title }} <#{{ step.anchor_id }}>`_
{% endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.. autoclass:: {{ step.module_path }}
:noindex:

.. setup-config-example:: {{ step.module_path }}

This file was deleted.

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
52 changes: 52 additions & 0 deletions tests/fixtures/usage_directive_html_partial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<section id="django-setup-config">
<h1>Setting up your application with django-setup-config<a class="headerlink" href="#django-setup-config" title="Link to this heading"></a></h1>
<p>You can use the included <code class="docutils literal notranslate"><span class="pre">setup_configuration</span></code> management command to configure your
instance from a yaml file as follows:</p>
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>python<span class="w"> </span>manage.py<span class="w"> </span>setup_configuration<span class="w"> </span>--yaml-file<span class="w"> </span>/path/to/config.yaml
</pre></div>
</div>
<p>You can also validate that the configuration source can be successfully loaded,
without actually running the steps, by adding the <code class="docutils literal notranslate"><span class="pre">validate-only</span></code> flag:</p>
<div class="highlight-bash notranslate"><div class="highlight"><pre><span></span>python<span class="w"> </span>manage.py<span class="w"> </span>setup_configuration<span class="w"> </span>--yaml-file<span class="w"> </span>/path/to/config.yaml<span class="w"> </span>--validate-only
</pre></div>
</div>
<p>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.</p>
<p>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.</p>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>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.</p>
</div>
<p>Further information can be found at the <a class="reference external" href="https://github.com/maykinmedia/django-setup-configuration/">django-setup-configuration</a> project page.</p>
<p>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):</p>
<ul class="simple">
<li><p><a class="reference external" href="#user-configuration">User Configuration</a></p></li>
</ul>
<section id="user-configuration">
<h2>User Configuration<a class="headerlink" href="#user-configuration" title="Link to this heading"></a></h2>
<dl class="py class">
<dt class="sig sig-object py">
<em class="property"><span class="pre">class</span><span class="w"> </span></em><span class="sig-prename descclassname"><span class="pre">testapp.configuration.</span></span><span class="sig-name descname"><span class="pre">UserConfigurationStep</span></span></dt>
<dd><p>Set up an initial user.</p>
</dd></dl>

<div class="highlight-yaml notranslate"><div class="highlight"><pre><span></span><span class="nt">user_configuration_enabled</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
<span class="nt">user_configuration</span><span class="p">:</span>

<span class="w"> </span><span class="c1"># DESCRIPTION: Required. 150 characters or fewer. Letters, digits and @/./+/-/_</span>
<span class="w"> </span><span class="c1"># only.</span>
<span class="w"> </span><span class="c1"># REQUIRED: true</span>
<span class="w"> </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">example_string</span>

<span class="w"> </span><span class="c1"># REQUIRED: true</span>
<span class="w"> </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">example_string</span>
</pre></div>
</div>
</section>
</section>
Loading

0 comments on commit 40f9e8f

Please sign in to comment.