Source code for yapconf.docs

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from yapconf.items import YapconfDictItem

# flake8: noqa
HEADER = """# {app_name} Configuration

This document describes the configuration for {app_name}. Each section will 
document a particular configuration value and its description. First, 
though, we start with the possible sources. 

This documentation was auto-generated by [yapconf.](https://github.com/loganasherjones/yapconf)
"""

CONFIG_HEADER = """## Configuration

This section outlines the various configuration items {app_name} supports.
"""

SOURCES_HEADER = """
## Sources

{app_name} configuration can be loaded from the below sources:
"""

SOURCE_TEMPLATE = """
#### {source_label}

{source_label} is of type {source_type_link}.

{source_type_description}
"""

ITEM_TEMPLATE = """### {name}

{description}

{attribute_table}
{item_options}

{long_description}
"""

SOURCE_TYPE_LINKS = {
    "dict": "https://yapconf.readthedocs.io/en/stable/sources.html#dict",
    "environment": "https://yapconf.readthedocs.io/en/stable/sources.html#environment",
    "etcd": "https://yapconf.readthedocs.io/en/stable/sources.html#etcd",
    "json": "https://yapconf.readthedocs.io/en/stable/sources.html#json",
    "kubernetes": "https://yapconf.readthedocs.io/en/stable/sources.html#kubernetes",
    "yaml": "https://yapconf.readthedocs.io/en/stable/sources.html#yaml",
}

VALID_JSON_LINK = "[valid JSON](https://www.json.org/)"
VALID_YAML_LINK = "[valid YAML](http://yaml.org/)"

DICT_DESCRIPTION = """
The dict source means that {app_name} will load configuration values from an 
in-memory dictionary that cannot be modified externally. Usually, it is used as 
a list of sensible defaults."""

ENVIRONMENT_DESCRIPTION = """
This environment source means that {app_name} will load configuration values 
by using their `env_name` that is documented below."""

ETCD_DESCRIPTION = """
The etcd source means that {app_name} will load configuration values from the 
`{key}` in etcd."""

JSON_DATA_DESCRIPTION = """
This json source means that {app_name} will load configuration values from an 
in-memory string and cannot be modified. Usually it is used as a list of 
sensible defaults."""

JSON_FILE_DESCRIPTION = """
This json source means that {app_name} will load configuration values from 
`{filename}`. You may edit this file with {valid_link}."""

K8S_DESCRIPTION = """The kubernetes source indicates that the configuration 
may be loaded from a kubernetes [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/). 
Specifically, {app_name} will load the configuration from the ConfigMap named 
`{config_map_name}` in the namespace `{config_map_namespace}`"""

K8S_KEY_DESCRIPTION = """
Additionally, {source_label} specified that `{key}` exists in the 
`{config_map_name}` ConfigMap. `{key}` will be loaded as `{config_type}`. 
You may modify this entry with {valid_link}."""

YAML_DESCRIPTION = """
The yaml source means that {app_name} will load configuration values from 
`{filename}`. You may edit this file with {valid_link}"""


def _find_row_maxes(headers, rows):
    maxes = {}
    for key, value in headers.items():
        maxes[key] = len(value)
    for row in rows:
        for key, value in row.items():
            maxes[key] = max(maxes[key], len(value))
    return maxes


def _get_padding(key, row_maxes, value, pad_value=" ", modifier=0):
    return pad_value * (row_maxes[key] - len(value) + modifier)


def _build_row(row, row_maxes, row_keys, pad_value=" "):
    row_value = "|"
    for key in row_keys:
        padding = _get_padding(key, row_maxes, row[key], pad_value)
        row_value = row_value + " " + row[key] + padding + " |"
    return row_value


def _build_separator(row_maxes, row_keys):
    fake_row = row_maxes.copy()
    for key, value in row_maxes.items():
        fake_row[key] = ""
    return _build_row(fake_row, row_maxes, row_keys, pad_value="-")


[docs]def build_markdown_table(headers, rows, row_keys=None): """Build a lined up markdown table. Args: headers (dict): A key -> value pairing fo the headers. rows (list): List of dictionaries that contain all the keys listed in the headers. row_keys (list): A sorted list of keys to display Returns: A valid Markdown Table as a string. """ row_maxes = _find_row_maxes(headers, rows) row_keys = row_keys or [key for key, value in headers.items()] table = [ _build_row(headers, row_maxes, row_keys), _build_separator(row_maxes, row_keys), ] for row in rows: table.append(_build_row(row, row_maxes, row_keys)) return "\n".join(table) + "\n"
def _generate_source_description(source, app_name, source_label): format_kwargs = {"app_name": app_name} template = "" if source.type == "dict": template = DICT_DESCRIPTION elif source.type == "environment": template = ENVIRONMENT_DESCRIPTION elif source.type == "etcd": template = ETCD_DESCRIPTION format_kwargs["key"] = source.key elif source.type == "json": if source.filename: template = JSON_FILE_DESCRIPTION format_kwargs["filename"] = source.filename format_kwargs["valid_link"] = VALID_JSON_LINK else: template = JSON_DATA_DESCRIPTION elif source.type == "kubernetes": template = K8S_DESCRIPTION format_kwargs["config_map_name"] = source.name format_kwargs["config_map_namespace"] = source.namespace if source.key: template = "\n".join([t for t in [template, K8S_KEY_DESCRIPTION]]) format_kwargs["source_label"] = source_label format_kwargs["key"] = source.key format_kwargs["config_type"] = source.config_type if source.config_type == "yaml": format_kwargs["valid_link"] = VALID_YAML_LINK else: format_kwargs["valid_link"] = VALID_JSON_LINK elif source.type == "yaml": template = YAML_DESCRIPTION format_kwargs["filename"] = source.filename format_kwargs["valid_link"] = VALID_YAML_LINK return template.format(**format_kwargs) def _generate_source_section(source_label, source, app_name): source_type_link = "[%s](%s)" % (source.type, SOURCE_TYPE_LINKS[source.type],) source_type_description = _generate_source_description( source, app_name, source_label ) return SOURCE_TEMPLATE.format( source_label=source_label, source_type_link=source_type_link, source_type_description=source_type_description, ) def _generate_item_table(item): headers = { "attribute": "Attribute", "value": "Value", } if item.cli_support: cli_names = item.cli_names else: cli_names = None rows = [ {"attribute": "**item_type**", "value": "`%s`" % item.item_type,}, {"attribute": "**default**", "value": "`%s`" % item.default,}, {"attribute": "**env_name**", "value": "`%s`" % item.env_name,}, {"attribute": "**required**", "value": "`%s`" % item.required,}, {"attribute": "**cli_name**", "value": "`%s`" % cli_names,}, {"attribute": "**fallback**", "value": "`%s`" % item.fallback}, {"attribute": "**choices**", "value": "`%s`" % item.choices,}, ] return build_markdown_table(headers, rows, ["attribute", "value"]) def _generate_item_options(item, app_name): options = [] if item.env_name: options.append( "You can set {name} from the environment by setting the " "environment variable `{env_name}`".format( name=item.fq_name, env_name=item.env_name ) ) if item.cli_support: cli_names = item.cli_names options.append( "You can set `{name}` from the command-line by specifying " "`{cli_names}` at {app_name}'s entrypoint.".format( name=item.fq_name, cli_names=cli_names, app_name=app_name, ) ) if item.fallback: option = ( "If `{name}` is not set in any of the sources listed, it will " "attempt to fallback to the value set in `{fallback}`." ).format(name=item.fq_name, fallback=item.fallback) if item.default: option += ( " If neither the fallback nor the original key is found " "then the value will fallback to the default of `{default}`" ).format(default=("%s" % item.default)) options.append(option) elif item.default: options.append( ( "If `{name}` is not set in any of the sources listed, it will " "fallback to the default value `{value}`".format( name=item.fq_name, value=item.default ) ) ) return "\n\n".join(options) def _generate_item_section(item, app_name): item_table = _generate_item_table(item) item_options = _generate_item_options(item, app_name) long_description = item.long_description or "" return ITEM_TEMPLATE.format( name=item.fq_name, description=item.description or "No description provided.", attribute_table=item_table, item_options=item_options, long_description=long_description, ) def _get_table_row_from_item(item): return { "name": "[" + item.fq_name + "](#" + item.fq_name + ")", "type": item.item_type, "default": "%s" % item.default, "description": "%s" % item.description, } def _generate_item_sections(items, app_name): rows = [] item_sections = [] for item in items: if isinstance(item, YapconfDictItem): tmp_rows, tmp_sections = _generate_item_sections( _sorted_dict_values(item.children), app_name ) rows += tmp_rows item_sections += tmp_sections else: rows.append(_get_table_row_from_item(item)) item_sections.append(_generate_item_section(item, app_name)) return rows, item_sections def _sorted_dict_values(dictionary): sorted_keys = sorted(list(dictionary)) return [dictionary[key] for key in sorted_keys]
[docs]def generate_markdown_doc(app_name, spec): """Generate Markdown Documentation for the given spec/app name. Args: app_name (str): The name of the application. spec (YapconfSpec): A yapconf specification with sources loaded. Returns (str): A valid, markdown string representation of the documentation for the given specification. """ # Apply standard headers. sections = [ HEADER.format(app_name=app_name), SOURCES_HEADER.format(app_name=app_name), ] # Generate the sources section of the documentation sorted_labels = sorted(list(spec.sources)) for label in sorted_labels: sections.append(_generate_source_section(label, spec.sources[label], app_name)) # Generate the config section. sections.append(CONFIG_HEADER.format(app_name=app_name)) table_rows, item_sections = _generate_item_sections( _sorted_dict_values(spec.items), app_name ) headers = { "name": "Name", "type": "Type", "default": "Default", "description": "Description", } sections.append( build_markdown_table( headers, table_rows, ["name", "type", "default", "description"], ) ) for item_section in item_sections: sections.append(item_section) return "\n".join([section for section in sections])