From 6c647aa26348665da37377720e1cf42aef7ccc7f Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 9 Jan 2024 16:23:08 +0100 Subject: [PATCH] Interpret double newlines as paragraph breaks in documentation strings. (#82465) --- .../82465-ansible-doc-paragraphs.yml | 2 + lib/ansible/cli/doc.py | 60 +++++++++++-------- .../testcol/plugins/modules/randommodule.py | 9 +++ .../ansible-doc/randommodule-text.output | 9 +++ .../targets/ansible-doc/randommodule.output | 4 ++ 5 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 changelogs/fragments/82465-ansible-doc-paragraphs.yml diff --git a/changelogs/fragments/82465-ansible-doc-paragraphs.yml b/changelogs/fragments/82465-ansible-doc-paragraphs.yml new file mode 100644 index 00000000000..a9fb63245e3 --- /dev/null +++ b/changelogs/fragments/82465-ansible-doc-paragraphs.yml @@ -0,0 +1,2 @@ +minor_changes: + - "ansible-doc - treat double newlines in documentation strings as paragraph breaks. This is useful to create multi-paragraph notes in module/plugin documentation (https://github.com/ansible/ansible/pull/82465)." diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index 403b8fedb34..f7e9a158068 100755 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -1058,6 +1058,14 @@ class DocCLI(CLI, RoleMixin): version_added = '%s of %s' % (version_added, version_added_collection) return 'version %s' % (version_added, ) + @staticmethod + def warp_fill(text, limit, initial_indent='', subsequent_indent='', **kwargs): + result = [] + for paragraph in text.split('\n\n'): + result.append(textwrap.fill(paragraph, limit, initial_indent=initial_indent, subsequent_indent=subsequent_indent, **kwargs)) + initial_indent = subsequent_indent + return '\n'.join(result) + @staticmethod def add_fields(text, fields, limit, opt_indent, return_values=False, base_indent=''): @@ -1083,11 +1091,11 @@ class DocCLI(CLI, RoleMixin): for entry_idx, entry in enumerate(opt['description'], 1): if not isinstance(entry, string_types): raise AnsibleError("Expected string in description of %s at index %s, got %s" % (o, entry_idx, type(entry))) - text.append(textwrap.fill(DocCLI.tty_ify(entry), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + text.append(DocCLI.warp_fill(DocCLI.tty_ify(entry), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) else: if not isinstance(opt['description'], string_types): raise AnsibleError("Expected string in description of %s, got %s" % (o, type(opt['description']))) - text.append(textwrap.fill(DocCLI.tty_ify(opt['description']), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + text.append(DocCLI.warp_fill(DocCLI.tty_ify(opt['description']), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) del opt['description'] suboptions = [] @@ -1179,9 +1187,9 @@ class DocCLI(CLI, RoleMixin): else: desc = doc['description'] - text.append("%s\n" % textwrap.fill(DocCLI.tty_ify(desc), - limit, initial_indent=opt_indent, - subsequent_indent=opt_indent)) + text.append("%s\n" % DocCLI.warp_fill(DocCLI.tty_ify(desc), + limit, initial_indent=opt_indent, + subsequent_indent=opt_indent)) if doc.get('options'): text.append("OPTIONS (= is mandatory):\n") DocCLI.add_fields(text, doc.pop('options'), limit, opt_indent) @@ -1197,7 +1205,7 @@ class DocCLI(CLI, RoleMixin): if k not in doc: continue if isinstance(doc[k], string_types): - text.append('%s: %s' % (k.upper(), textwrap.fill(DocCLI.tty_ify(doc[k]), + text.append('%s: %s' % (k.upper(), DocCLI.warp_fill(DocCLI.tty_ify(doc[k]), limit - (len(k) + 2), subsequent_indent=opt_indent))) elif isinstance(doc[k], (list, tuple)): text.append('%s: %s' % (k.upper(), ', '.join(doc[k]))) @@ -1230,8 +1238,8 @@ class DocCLI(CLI, RoleMixin): else: desc = doc.pop('description') - text.append("%s\n" % textwrap.fill(DocCLI.tty_ify(desc), limit, initial_indent=opt_indent, - subsequent_indent=opt_indent)) + text.append("%s\n" % DocCLI.warp_fill(DocCLI.tty_ify(desc), limit, initial_indent=opt_indent, + subsequent_indent=opt_indent)) if 'version_added' in doc: version_added = doc.pop('version_added') @@ -1269,8 +1277,8 @@ class DocCLI(CLI, RoleMixin): if doc.get('notes', False): text.append("NOTES:") for note in doc['notes']: - text.append(textwrap.fill(DocCLI.tty_ify(note), limit - 6, - initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) + text.append(DocCLI.warp_fill(DocCLI.tty_ify(note), limit - 6, + initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) text.append('') text.append('') del doc['notes'] @@ -1279,45 +1287,45 @@ class DocCLI(CLI, RoleMixin): text.append("SEE ALSO:") for item in doc['seealso']: if 'module' in item: - text.append(textwrap.fill(DocCLI.tty_ify('Module %s' % item['module']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify('Module %s' % item['module']), limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) description = item.get('description') if description is None and item['module'].startswith('ansible.builtin.'): description = 'The official documentation on the %s module.' % item['module'] if description is not None: - text.append(textwrap.fill(DocCLI.tty_ify(description), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(description), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) if item['module'].startswith('ansible.builtin.'): relative_url = 'collections/%s_module.html' % item['module'].replace('.', '/', 2) - text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent)) elif 'plugin' in item and 'plugin_type' in item: plugin_suffix = ' plugin' if item['plugin_type'] not in ('module', 'role') else '' - text.append(textwrap.fill(DocCLI.tty_ify('%s%s %s' % (item['plugin_type'].title(), plugin_suffix, item['plugin'])), + text.append(DocCLI.warp_fill(DocCLI.tty_ify('%s%s %s' % (item['plugin_type'].title(), plugin_suffix, item['plugin'])), limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) description = item.get('description') if description is None and item['plugin'].startswith('ansible.builtin.'): description = 'The official documentation on the %s %s%s.' % (item['plugin'], item['plugin_type'], plugin_suffix) if description is not None: - text.append(textwrap.fill(DocCLI.tty_ify(description), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(description), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) if item['plugin'].startswith('ansible.builtin.'): relative_url = 'collections/%s_%s.html' % (item['plugin'].replace('.', '/', 2), item['plugin_type']) - text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent)) elif 'name' in item and 'link' in item and 'description' in item: - text.append(textwrap.fill(DocCLI.tty_ify(item['name']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(item['name']), limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) - text.append(textwrap.fill(DocCLI.tty_ify(item['description']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(item['description']), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) - text.append(textwrap.fill(DocCLI.tty_ify(item['link']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(item['link']), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) elif 'ref' in item and 'description' in item: - text.append(textwrap.fill(DocCLI.tty_ify('Ansible documentation [%s]' % item['ref']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify('Ansible documentation [%s]' % item['ref']), limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent)) - text.append(textwrap.fill(DocCLI.tty_ify(item['description']), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(item['description']), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) - text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink('/#stq=%s&stp=1' % item['ref'])), + text.append(DocCLI.warp_fill(DocCLI.tty_ify(get_versioned_doclink('/#stq=%s&stp=1' % item['ref'])), limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' ')) text.append('') @@ -1326,14 +1334,14 @@ class DocCLI(CLI, RoleMixin): if doc.get('requirements', False): req = ", ".join(doc.pop('requirements')) - text.append("REQUIREMENTS:%s\n" % textwrap.fill(DocCLI.tty_ify(req), limit - 16, initial_indent=" ", subsequent_indent=opt_indent)) + text.append("REQUIREMENTS:%s\n" % DocCLI.warp_fill(DocCLI.tty_ify(req), limit - 16, initial_indent=" ", subsequent_indent=opt_indent)) # Generic handler for k in sorted(doc): if k in DocCLI.IGNORE or not doc[k]: continue if isinstance(doc[k], string_types): - text.append('%s: %s' % (k.upper(), textwrap.fill(DocCLI.tty_ify(doc[k]), limit - (len(k) + 2), subsequent_indent=opt_indent))) + text.append('%s: %s' % (k.upper(), DocCLI.warp_fill(DocCLI.tty_ify(doc[k]), limit - (len(k) + 2), subsequent_indent=opt_indent))) elif isinstance(doc[k], (list, tuple)): text.append('%s: %s' % (k.upper(), ', '.join(doc[k]))) else: @@ -1395,14 +1403,14 @@ def _do_yaml_snippet(doc): if module: if required: desc = "(required) %s" % desc - text.append(" %-20s # %s" % (o, textwrap.fill(desc, limit, subsequent_indent=subdent))) + text.append(" %-20s # %s" % (o, DocCLI.warp_fill(desc, limit, subsequent_indent=subdent))) else: if required: default = '(required)' else: default = opt.get('default', 'None') - text.append("%s %-9s # %s" % (o, default, textwrap.fill(desc, limit, subsequent_indent=subdent, max_lines=3))) + text.append("%s %-9s # %s" % (o, default, DocCLI.warp_fill(desc, limit, subsequent_indent=subdent, max_lines=3))) return text diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py index 51efdfd28bd..81e8fb86e6d 100644 --- a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py @@ -64,6 +64,15 @@ seealso: description: See also the Ansible docsite. - ref: foo_bar description: Some foo bar. +notes: + - This is a note. + - |- + This is a multi-paragraph note. + + This is its second paragraph. + This is just another line in the second paragraph. + Eventually this will break into a new line, + depending with which line width this is rendered. ''' EXAMPLES = ''' diff --git a/test/integration/targets/ansible-doc/randommodule-text.output b/test/integration/targets/ansible-doc/randommodule-text.output index ca361346c02..e496b44dc57 100644 --- a/test/integration/targets/ansible-doc/randommodule-text.output +++ b/test/integration/targets/ansible-doc/randommodule-text.output @@ -75,6 +75,15 @@ OPTIONS (= is mandatory): type: str +NOTES: + * This is a note. + * This is a multi-paragraph note. + This is its second paragraph. This is just another line + in the second paragraph. Eventually this will break into + a new line, depending with which line width this is + rendered. + + SEE ALSO: * Module ansible.builtin.ping The official documentation on the diff --git a/test/integration/targets/ansible-doc/randommodule.output b/test/integration/targets/ansible-doc/randommodule.output index f40202a826c..c4696ab7dac 100644 --- a/test/integration/targets/ansible-doc/randommodule.output +++ b/test/integration/targets/ansible-doc/randommodule.output @@ -21,6 +21,10 @@ "filename": "./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py", "has_action": false, "module": "randommodule", + "notes": [ + "This is a note.", + "This is a multi-paragraph note.\n\nThis is its second paragraph.\nThis is just another line in the second paragraph.\nEventually this will break into a new line,\ndepending with which line width this is rendered." + ], "options": { "sub": { "description": "Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this.",