From 00dec463b778800e55c764946403966f8415db67 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 21 May 2021 09:17:26 +0200 Subject: [PATCH] Generate role documentation. --- antsibull/augment_docs.py | 9 +- antsibull/cli/doc_commands/stable.py | 9 +- antsibull/data/docsite/list_of_plugins.rst.j2 | 3 + .../data/docsite/plugins_by_collection.rst.j2 | 19 +- antsibull/data/docsite/role.rst.j2 | 386 ++++++++++++++++++ antsibull/write_docs.py | 36 +- 6 files changed, 445 insertions(+), 17 deletions(-) create mode 100644 antsibull/data/docsite/role.rst.j2 diff --git a/antsibull/augment_docs.py b/antsibull/augment_docs.py index db052a512..6caa5797b 100644 --- a/antsibull/augment_docs.py +++ b/antsibull/augment_docs.py @@ -56,5 +56,10 @@ def augment_docs(plugin_info: t.MutableMapping[str, t.MutableMapping[str, t.Any] """ for plugin_type, plugin_map in plugin_info.items(): for plugin_name, plugin_record in plugin_map.items(): - add_full_key(plugin_record['return'], 'contains') - add_full_key(plugin_record['doc']['options'], 'suboptions') + if 'return' in plugin_record: + add_full_key(plugin_record['return'], 'contains') + if 'doc' in plugin_record: + add_full_key(plugin_record['doc']['options'], 'suboptions') + if 'entry_points' in plugin_record: + for entry_point in plugin_record['entry_points'].values(): + add_full_key(entry_point['options'], 'suboptions') diff --git a/antsibull/cli/doc_commands/stable.py b/antsibull/cli/doc_commands/stable.py index 89068bdc8..88a27fa4a 100644 --- a/antsibull/cli/doc_commands/stable.py +++ b/antsibull/cli/doc_commands/stable.py @@ -230,8 +230,13 @@ def get_plugin_contents(plugin_info: t.Mapping[str, t.Mapping[str, t.Any]], for plugin_type, plugin_list in plugin_info.items(): for plugin_name, plugin_desc in plugin_list.items(): namespace, collection, short_name = get_fqcn_parts(plugin_name) - plugin_contents[plugin_type]['.'.join((namespace, collection))][short_name] = ( - plugin_desc['doc']['short_description']) + if plugin_type == 'role': + desc = '' + if 'main' in plugin_desc['entry_points']: + desc = plugin_desc['entry_points']['main']['short_description'] + else: + desc = plugin_desc['doc']['short_description'] + plugin_contents[plugin_type]['.'.join((namespace, collection))][short_name] = desc return plugin_contents diff --git a/antsibull/data/docsite/list_of_plugins.rst.j2 b/antsibull/data/docsite/list_of_plugins.rst.j2 index fcaae0bf3..dcefed989 100644 --- a/antsibull/data/docsite/list_of_plugins.rst.j2 +++ b/antsibull/data/docsite/list_of_plugins.rst.j2 @@ -5,6 +5,9 @@ {% if plugin_type == 'module' %} Index of all Modules ==================== +{% elif plugin_type == 'role' %} +Index of all Roles +================== {% else %} Index of all @{ plugin_type | capitalize }@ Plugins =============@{ '=' * (plugin_type | length) }@======== diff --git a/antsibull/data/docsite/plugins_by_collection.rst.j2 b/antsibull/data/docsite/plugins_by_collection.rst.j2 index e1058846f..dedf0ea03 100644 --- a/antsibull/data/docsite/plugins_by_collection.rst.j2 +++ b/antsibull/data/docsite/plugins_by_collection.rst.j2 @@ -29,20 +29,35 @@ Collection version @{ collection_version }@ {% endfor %} +{% if 'role' in plugin_maps %} +Role Index +---------- + +These are the roles in the @{collection_name}@ collection + +{% for name, desc in plugin_maps['role'].items() | sort %} +* :ref:`@{ name }@ ` -- @{ desc | rst_ify | indent(width=2) }@ +{% endfor %} + +{% endif %} + Plugin Index ------------ -{% if plugin_maps %} +{% if plugin_maps | reject('eq', 'role') | list %} These are the plugins in the @{collection_name}@ collection {% else %} There are no plugins in the @{collection_name}@ collection with automatically generated documentation. {% endif %} -{% for category, plugins in plugin_maps.items() | sort %} +{% for category, plugins in plugin_maps.items() | sort | rejectattr('0', 'eq', 'role') %} {% if category == 'module' %} Modules ~~~~~~~ +{% elif category == 'role' %} +Roles +~~~~~ {% else %} @{ category | capitalize }@ Plugins @{ '~' * ((category | length) + 8) }@ diff --git a/antsibull/data/docsite/role.rst.j2 b/antsibull/data/docsite/role.rst.j2 new file mode 100644 index 000000000..a6917f7b0 --- /dev/null +++ b/antsibull/data/docsite/role.rst.j2 @@ -0,0 +1,386 @@ +.. Document meta + +:orphan: + +{# If we can put together source and github repo, we could make the Edit me of github button work. + See meta.get("source") in Ansible's docs/docsite/_themes/sphinx_rtd_theme/breadcrumbs.html + for more information +:source: @{ source }@ +-#} + +.. Anchors + +.. _ansible_collections.@{plugin_name}@_@{plugin_type}@: + +.. Anchors: aliases + +{# TODO: This assumes that aliases will be short names. + If they're FQCN, then we need to change this +#} + +.. Title + +{% if entry_points.main and entry_points.main.short_description -%} +{% set title = plugin_name + ' -- ' + entry_points.main.short_description | rst_ify -%} +{% else -%} +{% set title = plugin_name -%} +{% endif -%} + +@{ title }@ +@{ '+' * title|length }@ + +.. Collection note + +{% if collection == 'ansible.builtin' -%} +.. note:: + This module is part of ``ansible-base`` and included in all Ansible + installations. In most cases, you can use the short module name + @{ plugin_name.rsplit('.', 1)[-1] }@ even without specifying the ``collections:`` keyword. + Despite that, we recommend you use the FQCN for easy linking to the module + documentation and to avoid conflicting with other collections that may have + the same module name. +{% else %} +.. note:: + This role is part of the `@{collection}@ collection `_{% if collection_version %} (version @{ collection_version }@){% endif %}. + + To install it use: :code:`ansible-galaxy collection install @{collection}@`. + + To use it in a playbook, specify: :code:`@{plugin_name}@`. +{% endif %} + +.. contents:: + :local: + :depth: 2 + +{% for entry_point, ep_doc in entry_points.items() | sort %} + +.. Entry point title + +{% if ep_doc['short_description'] -%} +{% set title = 'Entry point ``' + entry_point + '`` -- ' + ep_doc['short_description'] | rst_ify -%} +{% else -%} +{% set title = 'Entry point ``' + entry_point + '``' -%} +{% endif -%} + +@{ title }@ +@{ '-' * title|length }@ + +.. version_added + +{% if ep_doc['version_added'] is still_relevant -%} +.. versionadded:: @{ ep_doc['version_added'] }@ +{% endif %} + +.. Deprecated + +{% if ep_doc['deprecated'] -%} +DEPRECATED +^^^^^^^^^^ +{% if ep_doc['deprecated']['removed_at_date'] %} +:Removed in: major release after @{ ep_doc['deprecated']['removed_at_date'] | rst_ify }@ +{% elif ep_doc['deprecated']['removed_in'] %} +:Removed in: version @{ ep_doc['deprecated']['removed_in'] | rst_ify }@ +{% else %} +:Removed in: a future release +{% endif %} +:Why: @{ ep_doc['deprecated']['why'] | rst_ify }@ +:Alternative: @{ ep_doc['deprecated']['alternative'] | rst_ify }@ +{% endif %} + +Synopsis +^^^^^^^^ + +.. Description + +{% for desc in ep_doc['description'] -%} +- @{ desc | rst_ify | indent(width=2) }@ +{% endfor %} + +.. Requirements + +{% if ep_doc['requirements'] -%} +Requirements +^^^^^^^^^^^^ +The below requirements are needed on the remote host and/or the local controller node. + +{% for req in ep_doc['requirements'] %} +- @{ req | rst_ify | indent(width=2) }@ +{% endfor %} + +{% endif %} + +.. Options + +{% if ep_doc['options'] -%} + +Parameters +^^^^^^^^^^ + +.. raw:: html + + + {# Pre-compute the nesting depth to allocate columns -#} + @{ to_kludge_ns('maxdepth', 1) -}@ + {% for key, value in ep_doc['options']|dictsort recursive -%} + @{ to_kludge_ns('maxdepth', [loop.depth, from_kludge_ns('maxdepth')] | max) -}@ + {% if value['suboptions'] -%} + @{ loop(value['suboptions'].items()) -}@ + {% endif -%} + {% endfor -%} + {# Header of the documentation -#} + + + + {% if plugin_type != 'module' %} + + {% endif %} + + + {% for key, value in ep_doc['options']|dictsort recursive %} + + {# indentation based on nesting level #} + {% for i in range(1, loop.depth) %} + + {% endfor %} + {# parameter name with required and/or introduced label #} + + {# default / choices #} + + {# configuration #} + {% if plugin_type != 'module' %} + + {% endif %} + {# description #} + + + {% if value['suboptions'] %} + @{ loop(value['suboptions']|dictsort) }@ + {% endif %} + {% endfor %} +
ParameterChoices/DefaultsConfigurationComments
+
+ @{ key }@ + +
+ @{ value['type'] | documented_type }@ + {% if value['type'] == 'list' and value['elements'] is not none %} / elements=@{ value['elements'] | documented_type }@{% endif %} + {% if value['required'] %} / required{% endif %} +
+ {% if value['version_added'] is still_relevant %} +
+ added in @{value['version_added']}@ of @{ value['version_added_collection'] | escape }@ +
+ {% endif %} + {% if plugin_type != 'module' and value['deprecated'] %} +
+ {% if value['deprecated']['removed_at_date'] %} + Removed in: major release after @{ value['deprecated']['removed_at_date'] | escape }@ + {% elif value['deprecated']['removed_in'] %} + Removed in: version @{ value['deprecated']['removed_in'] | escape }@ + {% else %} + Removed in: a future release + {% endif %} + {% if value['deprecated']['removed_from_collection'] and value['deprecated']['removed_from_collection'] != collection %} + of @{ value['deprecated']['removed_from_collection'] | escape }@ + {% endif %} +
+ Why: @{ value['deprecated']['why'] | escape }@ +
+ Alternative: @{ value['deprecated']['alternative'] | escape }@ +
+ {% endif %} +
+ {# Turn boolean values in 'yes' and 'no' values #} + {% if value['default'] is sameas true %} + {% set _x = value.update({'default': 'yes'}) %} + {% elif value['default'] is not none and value['default'] is sameas false %} + {% set _x = value.update({'default': 'no'}) %} + {% endif %} + {% if value['type'] == 'bool' %} + {% set _x = value.update({'choices': ['no', 'yes']}) %} + {% endif %} + {# Show possible choices and highlight details #} + {% if value['choices'] %} +
    Choices: + {% for choice in value['choices'] %} + {# Turn boolean values in 'yes' and 'no' values #} + {% if choice is sameas true %} + {% set choice = 'yes' %} + {% elif choice is sameas false %} + {% set choice = 'no' %} + {% endif %} + {% if (value['default'] is not list and value['default'] == choice) or (value['default'] is list and choice in value['default']) %} +
  • @{ choice | escape }@ ←
  • + {% else %} +
  • @{ choice | escape }@
  • + {% endif %} + {% endfor %} +
+ {% endif %} + {# Show default value, when multiple choice or no choices #} + {% if value['default'] is not none and value['default'] not in value['choices'] %} + Default:
@{ value['default'] | tojson | escape }@
+ {% endif %} +
+ {% if value['ini'] %} +
ini entries: + {% for ini in value['ini'] %} +

+ [@{ ini['section'] }@]
@{ ini['key'] }@ = @{ value['default'] | default('VALUE') }@ + {% if ini['version_added'] is still_relevant %} +

+ added in @{ini['version_added']}@ of @{ ini['version_added_collection'] | escape }@ +
+ {% endif %} + {% if ini['deprecated'] %} +
+ {% if ini['deprecated']['removed_at_date'] %} + Removed in: major release after @{ ini['deprecated']['removed_at_date'] | escape }@ + {% elif ini['deprecated']['removed_in'] %} + Removed in: version @{ ini['deprecated']['removed_in'] | escape }@ + {% else %} + Removed in: a future release + {% endif %} + {% if ini['deprecated']['removed_from_collection'] and ini['deprecated']['removed_from_collection'] != collection %} + of @{ ini['deprecated']['removed_from_collection'] | escape }@ + {% endif %} +
+ Why: @{ ini['deprecated']['why'] | escape }@ +
+ Alternative: @{ ini['deprecated']['alternative'] | escape }@ +
+ {% endif %} +

+ {% endfor %} +
+ {% endif %} + {% for env in value['env'] %} +
+ env:@{ env['name'] }@ + {% if env['version_added'] is still_relevant %} +
+ added in @{env['version_added']}@ of @{ env['version_added_collection'] | escape }@ +
+ {% endif %} + {% if env['deprecated'] %} +
+ {% if env['deprecated']['removed_at_date'] %} + Removed in: major release after @{ env['deprecated']['removed_at_date'] | escape }@ + {% elif env['deprecated']['removed_in'] %} + Removed in: version @{ env['deprecated']['removed_in'] | escape }@ + {% else %} + Removed in: a future release + {% endif %} + {% if env['deprecated']['removed_from_collection'] and env['deprecated']['removed_from_collection'] != collection %} + of @{ env['deprecated']['removed_from_collection'] | escape }@ + {% endif %} +
+ Why: @{ env['deprecated']['why'] | escape }@ +
+ Alternative: @{ env['deprecated']['alternative'] | escape }@ +
+ {% endif %} +
+ {% endfor %} + {% for myvar in value['vars'] %} +
+ var: @{ myvar['name'] }@ + {% if myvar['version_added'] is still_relevant %} +
+ added in @{myvar['version_added']}@ of @{ myvar['version_added_collection'] | escape }@ +
+ {% endif %} + {% if myvar['deprecated'] %} +
+ {% if myvar['deprecated']['removed_at_date'] %} + Removed in: major release after @{ myvar['deprecated']['removed_at_date'] | escape }@ + {% elif myvar['deprecated']['removed_in'] %} + Removed in: version @{ myvar['deprecated']['removed_in'] | escape }@ + {% else %} + Removed in: a future release + {% endif %} + {% if myvar['deprecated']['removed_from_collection'] and myvar['deprecated']['removed_from_collection'] != collection %} + of @{ myvar['deprecated']['removed_from_collection'] | escape }@ + {% endif %} +
+ Why: @{ myvar['deprecated']['why'] | escape }@ +
+ Alternative: @{ myvar['deprecated']['alternative'] | escape }@ +
+ {% endif %} +
+ {% endfor %} +
+ {% for desc in value['description'] %} +
@{ desc | replace('\n', '\n ') | html_ify }@
+ {% endfor %} + {% if value['aliases'] %} +

aliases: @{ value['aliases']|join(', ') }@
+ {% endif %} +
+
+{% endif %} + +.. Notes + +{% if ep_doc['notes'] -%} +Notes +^^^^^ + +.. note:: +{% for note in ep_doc['notes'] %} + - @{ note | rst_ify | indent(width=5) }@ +{% endfor %} +{% endif %} + +.. Seealso + +{% if ep_doc['seealso'] -%} +See Also +^^^^^^^^ + +.. seealso:: + +{% for item in ep_doc['seealso'] %} +{% if item.module is defined and item.description %} + @{ ('M(' + item['module'] + ')') | rst_ify }@ + @{ item['description'] | rst_ify }@ +{% elif item.module is defined %} + @{ ('M(' + item['module'] + ')') | rst_ify }@ + The official documentation on the **@{ item['module'] }@** module. +{% elif item.name is defined and item.link is defined and item.description %} + `@{ item['name'] }@ <@{ item['link'] }@>`_ + @{ item['description'] | rst_ify }@ +{% elif item.ref is defined and item.description %} + :ref:`@{ item['ref'] }@` + @{ item['description'] | rst_ify }@ +{% endif %} +{% endfor %} +{% endif %} + +{% if ep_doc['author'] -%} +Authors +^^^^^^^ + +{% for author_name in ep_doc['author'] %} +- @{ author_name }@ +{% endfor %} + +{% endif %} + +{% endfor %} + +.. Parsing errors + +{% if nonfatal_errors %} +There were some errors parsing the documentation for this plugin. Please file a bug with the collection. + +The errors were: + +{% for error in nonfatal_errors %} +* :: + +@{ error | indent(width=8, first=True) }@ + +{% endfor %} +{% endif %} diff --git a/antsibull/write_docs.py b/antsibull/write_docs.py index d836f71ea..ea7d87dd7 100644 --- a/antsibull/write_docs.py +++ b/antsibull/write_docs.py @@ -110,15 +110,24 @@ async def write_plugin_rst(collection_name: str, collection_meta: AnsibleCollect nonfatal_errors=nonfatal_errors ).error('{plugin_name} did not return correct RETURN or EXAMPLES.', plugin_name=plugin_name) - plugin_contents = plugin_tmpl.render( - collection=collection_name, - collection_version=collection_meta.version, - plugin_type=plugin_type, - plugin_name=plugin_name, - doc=plugin_record['doc'], - examples=plugin_record['examples'], - returndocs=plugin_record['return'], - nonfatal_errors=nonfatal_errors) + if plugin_type == 'role': + plugin_contents = plugin_tmpl.render( + collection=collection_name, + collection_version=collection_meta.version, + plugin_type=plugin_type, + plugin_name=plugin_name, + entry_points=plugin_record['entry_points'], + nonfatal_errors=nonfatal_errors) + else: + plugin_contents = plugin_tmpl.render( + collection=collection_name, + collection_version=collection_meta.version, + plugin_type=plugin_type, + plugin_name=plugin_name, + doc=plugin_record['doc'], + examples=plugin_record['examples'], + returndocs=plugin_record['return'], + nonfatal_errors=nonfatal_errors) if path_override is not None: plugin_file = path_override @@ -231,6 +240,7 @@ async def output_all_plugin_rst(collection_to_plugin_info: CollectionInfoT, env = doc_environment(('antsibull.data', 'docsite')) # Get the templates plugin_tmpl = env.get_template('plugin.rst.j2') + role_tmpl = env.get_template('role.rst.j2') error_tmpl = env.get_template('plugin-error.rst.j2') writers = [] @@ -238,6 +248,9 @@ async def output_all_plugin_rst(collection_to_plugin_info: CollectionInfoT, async with asyncio_pool.AioPool(size=lib_ctx.thread_max) as pool: for collection_name, plugins_by_type in collection_to_plugin_info.items(): for plugin_type, plugins in plugins_by_type.items(): + plugin_type_tmpl = plugin_tmpl + if plugin_type == 'role': + plugin_type_tmpl = role_tmpl for plugin_short_name, dummy_ in plugins.items(): plugin_name = '.'.join((collection_name, plugin_short_name)) writers.append(await pool.spawn( @@ -245,8 +258,9 @@ async def output_all_plugin_rst(collection_to_plugin_info: CollectionInfoT, collection_metadata[collection_name], plugin_short_name, plugin_type, plugin_info[plugin_type].get(plugin_name), - nonfatal_errors[plugin_type][plugin_name], plugin_tmpl, - error_tmpl, dest_dir, squash_hierarchy=squash_hierarchy))) + nonfatal_errors[plugin_type][plugin_name], + plugin_type_tmpl, error_tmpl, + dest_dir, squash_hierarchy=squash_hierarchy))) # Write docs for each plugin await asyncio.gather(*writers)