From 5629f83d6b85078f4d9a66421541d5f625dd21b5 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Fri, 5 Jul 2024 17:09:17 +0200 Subject: [PATCH] [#8] Define related_settings on ConfigSettings --- django_setup_configuration/__init__.py | 5 -- django_setup_configuration/config_settings.py | 24 ++++-- .../commands/generate_config_docs.py | 77 +++++++++++-------- docs/config_docs.rst | 13 +++- testapp/settings.py | 2 +- testapp/templates/testapp/config_doc.rst | 48 ------------ tests/mocks.py | 2 +- 7 files changed, 79 insertions(+), 92 deletions(-) delete mode 100644 testapp/templates/testapp/config_doc.rst diff --git a/django_setup_configuration/__init__.py b/django_setup_configuration/__init__.py index 588b1bb..e69de29 100644 --- a/django_setup_configuration/__init__.py +++ b/django_setup_configuration/__init__.py @@ -1,5 +0,0 @@ -from .config_settings import ConfigSettings - -__all__ = [ - "ConfigSettings", -] diff --git a/django_setup_configuration/config_settings.py b/django_setup_configuration/config_settings.py index b7ce0dc..8de6f93 100644 --- a/django_setup_configuration/config_settings.py +++ b/django_setup_configuration/config_settings.py @@ -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 @@ -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", @@ -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 @@ -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) @@ -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` diff --git a/django_setup_configuration/management/commands/generate_config_docs.py b/django_setup_configuration/management/commands/generate_config_docs.py index 99acd0a..06c61b1 100644 --- a/django_setup_configuration/management/commands/generate_config_docs.py +++ b/django_setup_configuration/management/commands/generate_config_docs.py @@ -1,3 +1,4 @@ +import itertools import pathlib from django.conf import settings @@ -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""" @@ -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: @@ -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""" @@ -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() @@ -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) diff --git a/docs/config_docs.rst b/docs/config_docs.rst index 681c068..223b0f4 100644 --- a/docs/config_docs.rst +++ b/docs/config_docs.rst @@ -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"], @@ -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", @@ -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: :: diff --git a/testapp/settings.py b/testapp/settings.py index 9a4fb68..4c4fe03 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -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" diff --git a/testapp/templates/testapp/config_doc.rst b/testapp/templates/testapp/config_doc.rst deleted file mode 100644 index 5c3ce56..0000000 --- a/testapp/templates/testapp/config_doc.rst +++ /dev/null @@ -1,48 +0,0 @@ -{% block link %}{{ link }}{% endblock %} - -{% block title %}{{ title }}{% endblock %} - -Settings Overview -================= - -Enable/Disable configuration: -""""""""""""""""""""""""""""" - -:: - - {% spaceless %} - {{ enable_settings }} - {% endspaceless %} - -{% if required_settings %} -Required: -""""""""" - -:: - - {% spaceless %} - {% for setting in required_settings %}{{ setting }} - {% endfor %} - {% endspaceless %} -{% endif %} - -All settings: -""""""""""""" - -:: - - {% spaceless %} - {% for setting in all_settings %}{{ setting }} - {% endfor %} - {% endspaceless %} - -Detailed Information -==================== - -:: - - {% spaceless %} - {% for detail in detailed_info %} - {% for part in detail %}{{ part|safe }} - {% endfor %}{% endfor %} - {% endspaceless %} diff --git a/tests/mocks.py b/tests/mocks.py index b2a99aa..1f7e0ce 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -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 (foo@bar.com)\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 (foo@bar.com)\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"