Skip to content

Commit d49dd6c

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

File tree

13 files changed

+374
-75
lines changed

13 files changed

+374
-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: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
def run(self):
30+
if not (settings := self._get_django_settings()):
31+
raise ValueError(
32+
"Unable to load Django settings. Is DJANGO_SETTINGS_MODULE set?"
33+
)
34+
35+
if not (configured_steps := settings.get("SETUP_CONFIGURATION_STEPS")):
36+
raise ValueError(
37+
"No steps configured. Set SETUP_CONFIGURATION_STEPS via your "
38+
"Django settings."
39+
)
40+
41+
usage_template = self._load_usage_template()
42+
step_template = self._load_step_template()
43+
steps = self._load_steps(configured_steps)
44+
45+
rst = ViewList()
46+
rst.append("", "<dynamic>")
47+
usage_rst = usage_template.render(context=Context({"steps": steps}))
48+
lines = usage_rst.split("\n")
49+
for line in lines:
50+
rst.append(line, "<dynamic>")
51+
52+
containing_node = nodes.section()
53+
containing_node["ids"] = ["django-setup-config"]
54+
containing_node += nodes.title(
55+
text="Setting up your application with django-setup-config"
56+
)
57+
self.state.nested_parse(rst, 0, containing_node)
58+
59+
for step in steps:
60+
rst = ViewList()
61+
step_node = nodes.section(ids=[step.anchor_id])
62+
step_node += nodes.title(text=step.title)
63+
64+
step_rst = step_template.render(context=Context({"step": step}))
65+
lines = step_rst.split("\n")
66+
rst.append("", "<dynamic>")
67+
for line in lines:
68+
rst.append(line, "<dynamic>")
69+
70+
self.state.nested_parse(rst, 0, step_node)
71+
containing_node += step_node
72+
73+
return [containing_node]
74+
75+
@classmethod
76+
def _get_django_settings(cls):
77+
from django.conf import settings
78+
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
79+
80+
try:
81+
return settings._wrapped.__dict__ if hasattr(settings, "_wrapped") else {}
82+
except (AppRegistryNotReady, AttributeError, ImproperlyConfigured):
83+
return {}
84+
85+
def _load_step_template(self):
86+
return Template((_TEMPLATES_PATH / "config_step.rst").read_text())
87+
88+
def _load_usage_template(self):
89+
return Template((_TEMPLATES_PATH / "config_doc.rst").read_text())
90+
91+
def _load_steps(self, configured_steps):
92+
steps_info = []
93+
for step_path in configured_steps:
94+
step_cls = import_string(step_path)
95+
step_info = StepInfo(
96+
title=step_cls.verbose_name,
97+
anchor_id=slugify(step_cls.verbose_name),
98+
module_path=step_path,
99+
step_cls=step_cls,
100+
)
101+
steps_info.append(step_info)
102+
return steps_info
103+
104+
105+
def setup(app):
106+
app.add_directive("setup-config-usage", SetupConfigUsageDirective)
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/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"sphinx.ext.autodoc",
5757
"sphinx.ext.todo",
5858
"django_setup_configuration.documentation.setup_config_example",
59+
"django_setup_configuration.documentation.setup_config_usage",
5960
]
6061

6162
# Add any paths that contain templates here, relative to this directory.

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

0 commit comments

Comments
 (0)