Skip to content

Commit

Permalink
[#8] Define related_settings on ConfigSettings
Browse files Browse the repository at this point in the history
  • Loading branch information
pi-sigma committed Jul 8, 2024
1 parent 9911b82 commit 5629f83
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 92 deletions.
5 changes: 0 additions & 5 deletions django_setup_configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
from .config_settings import ConfigSettings

__all__ = [
"ConfigSettings",
]
24 changes: 18 additions & 6 deletions django_setup_configuration/config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ class ConfigSettings:
models (`list`): a list of models from which documentation is retrieved
required_settings (`list`): required settings for a configuration step
optional_settings (`list`): optional settings for a configuration step
update_field_descriptions (`bool`): if `True`, custom model fields
independent (`bool`): if `True` (the default), the documentation will be
created in its own file; set to `False` if you want to avoid this and
plan to embed the docs in another file (see `related_config_settings`)
related_config_settings (`list`): optional list of `ConfigSettings` from
related configuration steps; used for embedding documentation
update_fields (`bool`): if `True`, custom model fields
(along with their descriptions) are loaded via the settings variable
`DJANGO_SETUP_CONFIG_CUSTOM_FIELDS`
additional_info (`dict`): information for configuration settings which are
Expand All @@ -61,6 +66,9 @@ class ConfigSettings:
"FOO_SOME_OPT_SETTING",
"FOO_SOME_OTHER_OPT_SETTING",
],
related_config_settings=[
"BarRelatedConfigurationStep.config_settings",
],
additional_info={
"example_non_model_field": {
"variable": "FOO_EXAMPLE_NON_MODEL_FIELD",
Expand All @@ -81,8 +89,10 @@ def __init__(
models: list[Type[models.Model]] | None = None,
required_settings: list[str],
optional_settings: list[str] | None = None,
independent: bool = True,
related_config_settings: list["ConfigSettings"] | None = None,
additional_info: dict[str, dict[str, str]] | None = None,
update_field_descriptions: bool = False,
update_fields: bool = False,
**kwargs,
):
self.enable_setting = enable_setting
Expand All @@ -91,15 +101,17 @@ def __init__(
self.models = models
self.required_settings = required_settings
self.optional_settings = optional_settings or []
self.independent = independent
self.related_config_settings = related_config_settings or []
self.additional_info = additional_info or {}
self.update_field_descriptions = update_field_descriptions
self.update_fields = update_fields
self.config_fields: list[ConfigField] = []

if not self.models:
return

if update_field_descriptions:
self.update_field_descriptions()
if update_fields:
self.load_additional_fields()

for model in self.models:
self.create_config_fields(model=model)
Expand Down Expand Up @@ -135,7 +147,7 @@ def get_default_value(field: models.Field) -> str:
return default

@staticmethod
def update_field_descriptions() -> None:
def load_additional_fields() -> None:
"""
Add custom fields + descriptions defined in settings to
`basic_field_descriptions`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import pathlib

from django.conf import settings
Expand All @@ -17,8 +18,16 @@ class ConfigDocBase:
"""

@staticmethod
def _add_additional_info(
config_settings: ConfigSettings, result: list[str]
def extract_unique_settings(settings: list[list[str]]) -> list[str]:
"""
Flatten `settings` (a list of lists with settings) and remove dupes
"""
unique_settings = set(itertools.chain.from_iterable(settings))
return list(unique_settings)

@staticmethod
def add_additional_info(
config_settings: ConfigSettings, result: list[list[str]]
) -> None:
"""Convenience/helper function to retrieve additional documentation info"""

Expand All @@ -41,9 +50,8 @@ def _add_additional_info(

def get_detailed_info(
self,
config: ConfigSettings,
config_step,
related_steps: list,
config_settings: ConfigSettings,
related_config_settings: list[ConfigSettings],
) -> list[list[str]]:
"""
Get information about the configuration settings:
Expand All @@ -52,22 +60,24 @@ def get_detailed_info(
3. from information provided manually in the `ConfigSettings` of related
configuration steps
"""
ret = []
for field in config.config_fields:
result = []
for field in config_settings.config_fields:
part = []
part.append(f"{'Variable':<20}{config.get_config_variable(field.name)}")
part.append(
f"{'Variable':<20}{config_settings.get_config_variable(field.name)}"
)
part.append(f"{'Setting':<20}{field.verbose_name}")
part.append(f"{'Description':<20}{field.description or 'No description'}")
part.append(f"{'Possible values':<20}{field.field_description}")
part.append(f"{'Default value':<20}{field.default_value}")
ret.append(part)
result.append(part)

self._add_additional_info(config, ret)
self.add_additional_info(config_settings, result)

for step in related_steps:
self._add_additional_info(step.config_settings, ret)
for config_settings in related_config_settings:
self.add_additional_info(config_settings, result)

return ret
return result

def format_display_name(self, display_name: str) -> str:
"""Surround title with '=' to display as heading in rst file"""
Expand All @@ -81,43 +91,47 @@ def render_doc(self, config_settings: ConfigSettings, config_step) -> str:
Render a `ConfigSettings` documentation template with the following variables:
1. enable_setting
2. required_settings
3. all_settings (required_settings + optional_settings)
3. all_settings (required_settings + optional_settings + related settings)
4. detailed_info
5. title
6. link (for crossreference across different files)
"""
# 1.
enable_setting = getattr(config_step, "enable_setting", None)
enable_setting = getattr(config_settings, "enable_setting", None)

# 2.
required_settings = [
name for name in getattr(config_settings, "required_settings", [])
]

# additional requirements from related configuration steps to embed
# additional settings from related configuration steps to embed
# the documentation of several steps into one
related_steps = [step for step in getattr(config_step, "related_steps", [])]
related_requirements_lists = [
step.config_settings.required_settings for step in related_steps
related_config_settings = [
config for config in getattr(config_settings, "related_config_settings", [])
]
related_requirements = set(
item for row in related_requirements_lists for item in row
required_settings_related = self.extract_unique_settings(
[config.required_settings for config in related_config_settings]
)
optional_settings_related = self.extract_unique_settings(
[config.optional_settings for config in related_config_settings]
)

required_settings.extend(list(related_requirements))
required_settings.extend(required_settings_related)
required_settings.sort()

# 3.
all_settings = [
setting
for setting in config_settings.required_settings
for setting in required_settings
+ config_settings.optional_settings
+ optional_settings_related
]
all_settings.sort()

# 4.
detailed_info = self.get_detailed_info(
config_settings, config_step, related_steps
config_settings,
related_config_settings,
)
detailed_info.sort()

Expand Down Expand Up @@ -167,10 +181,13 @@ def handle(self, *args, **kwargs) -> None:
for config_string in settings.SETUP_CONFIGURATION_STEPS:
config_step = import_string(config_string)

if config_settings := getattr(config_step, "config_settings", None):
doc_path = f"{target_dir}/{config_settings.file_name}.rst"
rendered_content = self.render_doc(config_settings, config_step)
config_settings = getattr(config_step, "config_settings", None)
if not config_settings or not config_settings.independent:
continue

doc_path = f"{target_dir}/{config_settings.file_name}.rst"
rendered_content = self.render_doc(config_settings, config_step)

if not self.content_is_up_to_date(rendered_content, doc_path):
with open(doc_path, "w+") as output:
output.write(rendered_content)
if not self.content_is_up_to_date(rendered_content, doc_path):
with open(doc_path, "w+") as output:
output.write(rendered_content)
13 changes: 12 additions & 1 deletion docs/config_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ attribute on the class:

FooConfigurationStep(BaseConfigurationStep):
verbose_name = "Configuration step for Foo"
enable_setting = "FOO_CONFIG_ENABLE"
config_settings = ConfigSettings(
enable_setting = "FOO_CONFIG_ENABLE"
namespace="FOO",
file_name="foo",
models=["FooConfigurationModel"],
Expand All @@ -72,6 +72,10 @@ attribute on the class:
"FOO_SOME_OPT_SETTING",
"FOO_SOME_OTHER_OPT_SETTING",
],
independent=True,
related_config_settings=[
"BarRelatedConfigurationStep.config_settings",
],
additional_info={
"example_non_model_field": {
"variable": "FOO_EXAMPLE_NON_MODEL_FIELD",
Expand All @@ -89,6 +93,13 @@ text of the relevant fields. You merely have to specify the models used in the c
step and which settings are required/optional. ``additional_info`` is used to manually document
configuration settings which are not associated with any model field.

In certain cases, you may want to avoid creating a separate documentation file for some
configuration steps. For example, you may want to include the documentation for API services
associated with ``FOO`` in the documentation for ``FOO``, instead of having a separate file
for each. In this case, you set ``independent`` to ``False`` on the ``ConfigSettings`` that you
want to embed, and include the relevant ``ConfigSettings`` under ``related_config_settings``
on your main config.

With everything set up, you can generate the docs with the following command:

::
Expand Down
2 changes: 1 addition & 1 deletion testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,5 @@
USER_CONFIGURATION_USERNAME = os.getenv("USER_CONFIGURATION_USERNAME", "demo")
USER_CONFIGURATION_PASSWORD = os.getenv("USER_CONFIGURATION_PASSWORD", "secret")

DJANGO_SETUP_CONFIG_TEMPLATE = "testapp/config_doc.rst"
DJANGO_SETUP_CONFIG_TEMPLATE = "django_setup_configuration/config_doc.rst"
DJANGO_SETUP_CONFIG_DOC_PATH = "testapp/docs/configuration"
48 changes: 0 additions & 48 deletions testapp/templates/testapp/config_doc.rst

This file was deleted.

2 changes: 1 addition & 1 deletion tests/mocks.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
mock_user_doc = '.. _user:\n\n==================\nUser Configuration\n==================\n\nSettings Overview\n=================\n\nEnable/Disable configuration:\n"""""""""""""""""""""""""""""\n\n::\n\n \n\n\nRequired:\n"""""""""\n\n::\n\n USER_CONFIGURATION_PASSWORD\n USER_CONFIGURATION_USERNAME\n\n\nAll settings:\n"""""""""""""\n\n::\n\n USER_CONFIGURATION_EMAIL\n USER_CONFIGURATION_FIRST_NAME\n USER_CONFIGURATION_IS_STAFF\n USER_CONFIGURATION_IS_SUPERUSER\n USER_CONFIGURATION_LAST_NAME\n USER_CONFIGURATION_PASSWORD\n USER_CONFIGURATION_USERNAME\n\nDetailed Information\n====================\n\n::\n\n Variable USER_CONFIGURATION_EMAIL\n Setting email address\n Description No description\n Possible values string representing an Email address ([email protected])\n Default value No default\n \n Variable USER_CONFIGURATION_FIRST_NAME\n Setting first name\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_IS_STAFF\n Setting staff status\n Description Designates whether the user can log into this admin site.\n Possible values True, False\n Default value False\n \n Variable USER_CONFIGURATION_IS_SUPERUSER\n Setting superuser status\n Description Designates that this user has all permissions without explicitly assigning them.\n Possible values True, False\n Default value False\n \n Variable USER_CONFIGURATION_LAST_NAME\n Setting last name\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_PASSWORD\n Setting password\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_USERNAME\n Setting username\n Description Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.\n Possible values string\n Default value No default\n' # noqua
mock_user_doc = '.. _user:\n\n==================\nUser Configuration\n==================\n\nSettings Overview\n=================\n\n\nEnable/Disable configuration:\n"""""""""""""""""""""""""""""\n\n::\n\n USER_CONFIGURATION_ENABLED\n\n\n\nRequired:\n"""""""""\n\n::\n\n USER_CONFIGURATION_PASSWORD\n USER_CONFIGURATION_USERNAME\n\n\nAll settings:\n"""""""""""""\n\n::\n\n USER_CONFIGURATION_EMAIL\n USER_CONFIGURATION_FIRST_NAME\n USER_CONFIGURATION_IS_STAFF\n USER_CONFIGURATION_IS_SUPERUSER\n USER_CONFIGURATION_LAST_NAME\n USER_CONFIGURATION_PASSWORD\n USER_CONFIGURATION_USERNAME\n\nDetailed Information\n====================\n\n::\n\n Variable USER_CONFIGURATION_EMAIL\n Setting email address\n Description No description\n Possible values string representing an Email address ([email protected])\n Default value No default\n \n Variable USER_CONFIGURATION_FIRST_NAME\n Setting first name\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_IS_STAFF\n Setting staff status\n Description Designates whether the user can log into this admin site.\n Possible values True, False\n Default value False\n \n Variable USER_CONFIGURATION_IS_SUPERUSER\n Setting superuser status\n Description Designates that this user has all permissions without explicitly assigning them.\n Possible values True, False\n Default value False\n \n Variable USER_CONFIGURATION_LAST_NAME\n Setting last name\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_PASSWORD\n Setting password\n Description No description\n Possible values string\n Default value No default\n \n Variable USER_CONFIGURATION_USERNAME\n Setting username\n Description Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.\n Possible values string\n Default value No default\n' # noqua
mock_user_doc_mismatch = "hello world"

0 comments on commit 5629f83

Please sign in to comment.