Skip to content

Commit b6a7ec9

Browse files
committed
Add a directives for general purpose documentation in downstream projects
1 parent 24e287e commit b6a7ec9

File tree

12 files changed

+371
-75
lines changed

12 files changed

+371
-75
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
include *.rst
22
include LICENSE
33
include django_setup_configuration/py.typed
4+
include django_setup_configuration/documentation/templates/*.rst
45
recursive-include django_setup_configuration *.html
56
recursive-include django_setup_configuration *.txt
67
recursive-include django_setup_configuration *.po
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from django.template import Template
5+
from django.template.context import Context
6+
from django.utils.module_loading import import_string
7+
from django.utils.text import slugify
8+
9+
from docutils import nodes
10+
from docutils.parsers.rst import Directive
11+
from docutils.statemachine import ViewList
12+
13+
from django_setup_configuration.configuration import BaseConfigurationStep
14+
15+
_TEMPLATES_PATH = Path(__file__).parent / "templates"
16+
17+
18+
@dataclass(frozen=True)
19+
class StepInfo:
20+
title: str
21+
anchor_id: str
22+
module_path: str
23+
step_cls: BaseConfigurationStep
24+
25+
26+
class SetupConfigUsageDirective(Directive):
27+
has_content = True
28+
29+
@classmethod
30+
def _get_django_settings(cls):
31+
from django.conf import settings
32+
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
33+
34+
try:
35+
return settings._wrapped.__dict__ if hasattr(settings, "_wrapped") else {}
36+
except (AppRegistryNotReady, AttributeError, ImproperlyConfigured):
37+
return {}
38+
39+
def run(self):
40+
if not (settings := self._get_django_settings()):
41+
raise ValueError(
42+
"Unable to load Django settings. Is DJANGO_SETTINGS_MODULE set?"
43+
)
44+
45+
if not (configured_steps := settings.get("SETUP_CONFIGURATION_STEPS")):
46+
raise ValueError(
47+
"No steps configured. Set SETUP_CONFIGURATION_STEPS in your "
48+
" via your Django settings."
49+
)
50+
51+
usage_template = Template((_TEMPLATES_PATH / "config_doc.rst").read_text())
52+
step_template = Template((_TEMPLATES_PATH / "config_step.rst").read_text())
53+
54+
steps_info = []
55+
for step_path in configured_steps:
56+
step_cls = import_string(step_path)
57+
step_info = StepInfo(
58+
title=step_cls.verbose_name,
59+
anchor_id=slugify(step_cls.verbose_name),
60+
module_path=step_path,
61+
step_cls=step_cls,
62+
)
63+
steps_info.append(step_info)
64+
65+
usage_rendered = usage_template.render(context=Context({"steps": steps_info}))
66+
67+
rst = ViewList()
68+
rst.append("", "<dynamic>")
69+
70+
lines = usage_rendered.split("\n")
71+
for line in lines:
72+
rst.append(line, "<dynamic>")
73+
74+
section_node = nodes.section()
75+
section_node["ids"] = ["django-setup-config"]
76+
section_node += nodes.title(
77+
text="Setting up your application with django-setup-config"
78+
)
79+
self.state.nested_parse(rst, 0, section_node)
80+
81+
for step in steps_info:
82+
rst = ViewList()
83+
subsection_node = nodes.section(ids=[step.anchor_id])
84+
subsection_node += nodes.title(text=step.title)
85+
86+
text = step_template.render(context=Context({"step": step}))
87+
lines = text.split("\n")
88+
89+
rst.append("", "<dynamic>") # Blank line
90+
for line in lines:
91+
rst.append(line, "<dynamic>")
92+
93+
self.state.nested_parse(rst, 0, subsection_node)
94+
section_node += subsection_node
95+
96+
# self.state.nested_parse(rst, 0, section_node)
97+
return [section_node]
98+
99+
100+
def setup(app):
101+
app.add_directive("setup-config-usage", SetupConfigUsageDirective)
102+
103+
return {"version": "1.0", "parallel_read_safe": True}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
You can use the included ``setup_configuration`` management command to configure your
2+
instance from a yaml file as follows:
3+
4+
.. code-block:: bash
5+
6+
python manage.py setup_configuration --yaml-file /path/to/config.yaml
7+
8+
You can also validate that the configuration source can be successfully loaded,
9+
without actually running the steps, by adding the ``validate-only`` flag:
10+
11+
.. code-block:: bash
12+
13+
python manage.py setup_configuration --yaml-file /path/to/config.yaml --validate-only
14+
15+
Both commands will either return 0 and a success message if the configuration file can
16+
be loaded without issues, otherwise it will return a non-zero exit code and print any
17+
validation errors.
18+
19+
Your YAML file should contain both a flag indicating whether the step is enabled or
20+
disabled, as well as an object containing the actual configuration values under the
21+
appropriate key.
22+
23+
.. note:: All steps are disabled by default. You only have to explicitly include the
24+
flag to enable a step, not to disable it, though you may do so if you wish to
25+
have an explicit record of what steps are disabled.
26+
27+
Further information can be found at the `django-setup-configuration
28+
<https://github.com/maykinmedia/django-setup-configuration/>`_ project page.
29+
30+
This projects includes the following configuration steps (click on each step for a
31+
brief descripion and an example YAML you can include in your config file):
32+
33+
{% for step in steps %}
34+
- `{{ step.title }} <#{{ step.anchor_id }}>`_
35+
{% endfor %}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.. autoclass:: {{ step.module_path }}
2+
:noindex:
3+
4+
.. setup-config-example:: {{ step.module_path }}

django_setup_configuration/templates/django_setup_configuration/config_doc.rst

Lines changed: 0 additions & 50 deletions
This file was deleted.

docs/config_docs.rst

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,85 @@
11
.. _config_docs:
22

3-
Configuration documentation
3+
Configuration Documentation
44
===========================
55

6-
The library provides a Sphinx directive that generates (and validates) an example configuration
7-
file in YAML format for a given ``ConfigurationStep``, containing information about the names of the fields,
8-
possible values, default values, and a short description. This helps clients determine
9-
what can be configured with the help of the library and how.
6+
The library provides two Sphinx directives:
107

8+
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.
119

12-
Setup
13-
"""""
10+
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.
1411

15-
Start by adding the following extension to ``conf.py`` in the documentation directory:
12+
Using setup-config-example
13+
--------------------------
1614

17-
::
15+
First, add the extension and its requirements to ``conf.py`` in your documentation directory:
16+
17+
.. code-block:: python
1818
1919
extensions = [
2020
...
21+
"sphinx.ext.autodoc",
2122
"django_setup_configuration.documentation.setup_config_example",
2223
...
2324
]
2425
25-
And then display a YAML example by using the directive:
2626
27-
::
27+
Then display a YAML example using the directive:
28+
29+
.. code-block:: rst
2830
2931
.. setup-config-example:: path.to.your.ConfigurationStep
3032
31-
which will produce something like the following example (in the case of the ``SitesConfigurationStep`` provided by this library):
33+
This will produce output similar to the following example (using the ``SitesConfigurationStep`` provided by this library):
3234

3335
.. setup-config-example:: django_setup_configuration.contrib.sites.steps.SitesConfigurationStep
3436

3537
.. warning::
3638

37-
Not all possible configurations are supported by this directive currently.
38-
More complex type annotations like ``list[ComplexObject | ComplexObject]`` will raise errors when
39-
trying to build the documentation
39+
Not all configurations are currently supported by this directive.
40+
Complex type annotations like ``list[ComplexObject | ComplexObject]`` will raise errors during documentation build.
41+
42+
Using setup-config-usage
43+
------------------------
44+
45+
First, add the extension and its requirements to ``conf.py`` in your documentation directory:
46+
47+
.. code-block:: python
48+
49+
extensions = [
50+
...
51+
"sphinx.ext.autodoc",
52+
"django_setup_configuration.documentation.setup_config_example",
53+
"django_setup_configuration.documentation.setup_config_usage",
54+
...
55+
]
56+
57+
To use this directive, you'll also have to ensure Django is configured and initialized
58+
in your Sphinx `conf.py` file, for instance like this:
59+
60+
.. code-block:: python
61+
62+
# docs/conf.py
63+
64+
# ...
65+
import django
66+
from django.conf import settings
67+
68+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_settings_module")
69+
django.setup()
70+
71+
# ...
72+
# extensions = [...]
73+
74+
Then display usage information using the directive:
75+
76+
.. code-block:: rst
77+
78+
.. setup-config-usage::
79+
80+
This generates a "how to" introduction for invoking the management command, followed by sections for each configured step with expected YAML configurations.
81+
82+
.. note::
83+
84+
For clarity, create a separate RST file (e.g., ``setup-configuration.rst``) containing only this directive.
85+
Sphinx will include the subsections for each step in your documentation's navigation area.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ tests = [
5252
"isort",
5353
"black",
5454
"flake8",
55-
"python-decouple"
55+
"python-decouple",
56+
"sphinx"
5657
]
5758
coverage = [
5859
"pytest-cov",

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
from pathlib import Path
23
from unittest import mock
34

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

16+
pytest_plugins = ("sphinx.testing.fixtures",)
17+
18+
19+
@pytest.fixture(scope="session")
20+
def rootdir() -> Path:
21+
return Path(__file__).resolve().parent / "sphinx-roots"
22+
1523

1624
@pytest.fixture
1725
def yaml_file_factory(tmp_path_factory):
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<section id="django-setup-config">
2+
<h1>Setting up your application with django-setup-config<a class="headerlink" href="#django-setup-config" title="Link to this heading"></a></h1>
3+
<p>You can use the included <code class="docutils literal notranslate"><span class="pre">setup_configuration</span></code> management command to configure your
4+
instance from a yaml file as follows:</p>
5+
<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
6+
</pre></div>
7+
</div>
8+
<p>You can also validate that the configuration source can be successfully loaded,
9+
without actually running the steps, by adding the <code class="docutils literal notranslate"><span class="pre">validate-only</span></code> flag:</p>
10+
<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
11+
</pre></div>
12+
</div>
13+
<p>Both commands will either return 0 and a success message if the configuration file can
14+
be loaded without issues, otherwise it will return a non-zero exit code and print any
15+
validation errors.</p>
16+
<p>Your YAML file should contain both a flag indicating whether the step is enabled or
17+
disabled, as well as an object containing the actual configuration values under the
18+
appropriate key.</p>
19+
<div class="admonition note">
20+
<p class="admonition-title">Note</p>
21+
<p>All steps are disabled by default. You only have to explicitly include the
22+
flag to enable a step, not to disable it, though you may do so if you wish to
23+
have an explicit record of what steps are disabled.</p>
24+
</div>
25+
<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>
26+
<p>This projects includes the following configuration steps (click on each step for a
27+
brief descripion and an example YAML you can include in your config file):</p>
28+
<ul class="simple">
29+
<li><p><a class="reference external" href="#user-configuration">User Configuration</a></p></li>
30+
</ul>
31+
<section id="user-configuration">
32+
<h2>User Configuration<a class="headerlink" href="#user-configuration" title="Link to this heading"></a></h2>
33+
<dl class="py class">
34+
<dt class="sig sig-object py">
35+
<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>
36+
<dd><p>Set up an initial user.</p>
37+
</dd></dl>
38+
39+
<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>
40+
<span class="nt">user_configuration</span><span class="p">:</span>
41+
42+
<span class="w"> </span><span class="c1"># DESCRIPTION: Required. 150 characters or fewer. Letters, digits and @/./+/-/_</span>
43+
<span class="w"> </span><span class="c1"># only.</span>
44+
<span class="w"> </span><span class="c1"># REQUIRED: true</span>
45+
<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>
46+
47+
<span class="w"> </span><span class="c1"># REQUIRED: true</span>
48+
<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>
49+
</pre></div>
50+
</div>
51+
</section>
52+
</section>

0 commit comments

Comments
 (0)