Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨[#64] add subgroup to generate_envvar_docs #71

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions open_api_framework/conf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class EnvironmentVariable:
default: Any
help_text: str
group: Optional[str] = None
sub_group: Optional[str] = None
auto_display_default: bool = True

def __post_init__(self):
Expand All @@ -34,6 +35,7 @@ def config(
default: Any = undefined,
help_text="",
group=None,
sub_group=None,
add_to_docs=True,
auto_display_default=True,
*args,
Expand All @@ -56,6 +58,7 @@ def config(
:param help_text: The help text to be displayed for this variable in the documentation. Default `""`
:param group: The name of the section under which this variable will be grouped. Default ``None``
:param sub_group: The name of the subsection under which this variable will be grouped. Default ``None``
:param add_to_docs: Whether or not this variable will be displayed in the documentation. Default ``True``
:param auto_display_default: Whether or not the passed ``default`` value is displayed in the docs, this can be
set to ``False`` in case a default needs more explanation that can be added to the ``help_text``
Expand All @@ -67,6 +70,7 @@ def config(
default=default,
help_text=help_text,
group=group,
sub_group=sub_group,
auto_display_default=auto_display_default,
)
if variable not in ENVVAR_REGISTRY:
Expand Down
179 changes: 173 additions & 6 deletions open_api_framework/management/commands/generate_envvar_docs.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,164 @@
import warnings
from collections import defaultdict

from django.conf import settings
from django.core.management.base import BaseCommand
from django.template import loader
from django.utils.module_loading import import_string

from django_setup_configuration.config_settings import ConfigSettings
from django_setup_configuration.management.commands.generate_config_docs import (
ConfigDocBase,
)

from open_api_framework.conf.utils import EnvironmentVariable


def convert_variables_to_rst(variables: list[EnvironmentVariable]) -> str:
template = loader.get_template("open_api_framework/env_config.rst")
grouped_vars = defaultdict(list)
grouped_vars = defaultdict(lambda: defaultdict(list))
for var in variables:
if not var.help_text:
warnings.warn(f"missing help_text for environment variable {var}")
grouped_vars[var.group].append(var)
return template.render({"vars": grouped_vars.items()})
grouped_vars[var.group][var.sub_group].append(var)

vars = []
for group, group_vars in grouped_vars.items():
vars.append((group, group_vars.items()))
return template.render({"vars": vars})


class SetupConfigDocs(ConfigDocBase):

def generate_config_file(self) -> str:

full_rendered_content = ""

if not hasattr(settings, "SETUP_CONFIGURATION_STEPS"):
return full_rendered_content

for config_string in settings.SETUP_CONFIGURATION_STEPS:
config_step = import_string(config_string)

config_settings = getattr(config_step, "config_settings", None)
if not config_settings or not config_settings.independent:
continue

rendered_content = self.render_doc(config_settings, config_step)
full_rendered_content += rendered_content

template = loader.get_template("open_api_framework/setup_config.rst")
rendered = template.render(
{"rendered_configuration_steps": full_rendered_content}
)

return rendered

def render_doc(self, config_settings, config_step) -> str:
"""
Render a `ConfigSettings` documentation template with the following variables:
1. enable_setting
2. required_settings
3. optional_settings
4. detailed_info
5. title
"""
# 1.
enable_setting = getattr(config_settings, "enable_setting", None)

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

# additional settings from related configuration steps to embed
# the documentation of several steps into one
related_config_settings = [
config for config in getattr(config_settings, "related_config_settings", [])
]
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(required_settings_related)
required_settings.sort()

optional_settings = config_settings.optional_settings
optional_settings.sort()

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

# 5.
title = self.format_display_name(config_step.verbose_name)

template_variables = {
"enable_setting": enable_setting,
"required_settings": required_settings,
"optional_settings": optional_settings,
"detailed_info": detailed_info,
"title": title,
}

template = loader.get_template(
"open_api_framework/components/setup_config_step.rst"
)
rendered = template.render(template_variables)

return rendered

def format_display_name(self, display_name: str) -> str:
"""Underlines title with '=' to display as heading in rst file"""

heading_bar = "-" * len(display_name)
display_name_formatted = f"{display_name}\n{heading_bar}"
return display_name_formatted

def get_detailed_info(
self,
config_settings: ConfigSettings,
related_config_settings: list[ConfigSettings],
) -> dict[dict[str]]:
"""
Get information about the configuration settings:
1. from model fields associated with the `ConfigSettings`
2. from information provided manually in the `ConfigSettings`
3. from information provided manually in the `ConfigSettings` of related
configuration steps
"""
result = dict()
for field in config_settings.config_fields:
part = dict()
variable = config_settings.get_config_variable(field.name)
part["setting"] = field.verbose_name
part["description"] = field.description or "No description"
part["possible_values"] = field.field_description
part["default_value"] = field.default_value

result[variable] = part

self.add_additional_info(config_settings, result)
for config_settings in related_config_settings:
self.add_additional_info(config_settings, result)

return result

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

additional_info = config_settings.additional_info

for key, value in additional_info.items():
result[key] = value


class Command(BaseCommand):
Expand All @@ -24,21 +168,32 @@ def add_arguments(self, parser):
super().add_arguments(parser)

parser.add_argument(
"--file",
help="Name and path of the file to which the documentation will be written.",
"--envvar-file",
help="Name and path of the file to which the envvar documentation will be written.",
nargs="?",
default="docs/env_config.rst",
)
parser.add_argument(
"--config-file",
help="Name and path of the file to which the setup configuration documentation will be written.",
nargs="?",
default="docs/setup_config.rst",
)
parser.add_argument(
"--exclude-group",
help="Names of groups that should not be excluded in the generated docs.",
action="append",
)

def handle(self, *args, **options):
self.generate_regular_config_docs(*args, **options)
self.generate_setup_config_docs(*args, **options)

@staticmethod
def generate_regular_config_docs(*args, **options):
from open_api_framework.conf.utils import ENVVAR_REGISTRY

file_path = options["file"]
file_path = options["envvar_file"]
exclude_groups = options["exclude_group"] or []

def _sort(envvar):
Expand All @@ -56,3 +211,15 @@ def _sort(envvar):
)
with open(file_path, "w") as f:
f.write(convert_variables_to_rst(sorted_registry))

@staticmethod
def generate_setup_config_docs(*args, **options) -> None:

file_path = options["config_file"]
doc_generator = SetupConfigDocs()

full_rendered_content = doc_generator.generate_config_file()

if len(full_rendered_content) > 0:
with open(file_path, "w") as f:
f.write(full_rendered_content)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% load doc_tags %}
{% spaceless %}
{{ title }}

Enable/Disable configuration:
"""""""""""""""""""""""""""""

::

{{ enable_setting }}

{% if required_settings %}
Required settings
"""""""""""""""""
{% for setting in required_settings %}
* ``{{ setting }}``: {% config_var_description setting detailed_info %}
{% endfor %}{% endif %}

{% if optional_settings %}
Optional Settings
"""""""""""""""""
{% for setting in optional_settings %}
* ``{{ setting }}``: {% config_var_description setting detailed_info %}
{% endfor %}{% endif %}

{% endspaceless %}
12 changes: 9 additions & 3 deletions open_api_framework/templates/open_api_framework/env_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ Environment configuration reference
Available environment variables
===============================

{% for group_name, vars in vars %}
{% for group_name, group_vars in vars %}
{{group_name}}
{{group_name|repeat_char:"-"}}

{% for var in vars %}* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %}
{% endfor %}
{% spaceless %}{% for subgroup_name, subgroup_vars in group_vars %}{% if subgroup_name is not null %}
{{subgroup_name}}
{{subgroup_name|repeat_char:"^"}}{% endif %}

{% spaceless %}
{% for var in subgroup_vars %}
* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %}{% endfor %}{% endspaceless %}{% endfor %}{% endspaceless %}

{% endfor %}

{% block extra %}{% endblock %}
Expand Down
16 changes: 16 additions & 0 deletions open_api_framework/templates/open_api_framework/setup_config.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% load doc_tags %}.. _installation_setup_config:

=============================
Setup configuration reference
=============================

{% block intro %}
Setup configuration description
{% endblock %}

Configuration Environment Variables
===================================

All configuration steps currently available:

{{ rendered_configuration_steps | safe }}
10 changes: 10 additions & 0 deletions open_api_framework/templatetags/doc_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ def ensure_endswith(value, char):
if not value.endswith(char):
value += char
return value


@register.simple_tag
def config_var_description(variable, details):

var_details = details.get(variable)
if var_details:
return var_details.get("description", "No description provided")

return "No description provided"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dependencies = [
"celery>=5.4.0",
"flower>=2.0.1",
"maykin-2fa>=1.0.1",
"django-setup-configuration>=0.1.0",
"django-setup-configuration>=0.3.0",
]

[project.urls]
Expand Down
8 changes: 8 additions & 0 deletions testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@

ROOT_URLCONF = "testapp.urls"

NOTIF_CONFIG_ENABLE = config(
"NOTIF_CONFIG_ENABLE",
default=False,
group="Setup Configuration",
sub_group="Notification Configuration",
help_text="Enable Notification Configuration",
)

# These are excluded from generate_envvar_docs test by their group
VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED1", "foo", group="Excluded")
VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED2", "bar", group="Excluded")
13 changes: 12 additions & 1 deletion tests/test_generate_envvar_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
``so-secret-i-cant-believe-you-are-looking-at-this``.


Setup Configuration
-------------------

Notification Configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^

* ``NOTIF_CONFIG_ENABLE``: Enable Notification Configuration. Defaults to: ``False``.


Optional
--------

Expand Down Expand Up @@ -72,7 +81,9 @@ def test_generate_envvar_docs():
"open_api_framework.management.commands.generate_envvar_docs.open", mock_file
):
call_command(
"generate_envvar_docs", file="some/file/path.txt", exclude_group="Excluded"
"generate_envvar_docs",
envvar_file="some/file/path.txt",
exclude_group="Excluded",
)

mock_file.assert_called_once_with("some/file/path.txt", "w")
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extras =
deps =
django42: Django~=4.2.0
commands =
py.test tests \
py.test tests -vv \
--cov --cov-report xml:reports/coverage-{envname}.xml \
{posargs}

Expand Down
Loading