From a2dc5fcc7da366e9d2c541863a7de2b0424ea773 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 21 Mar 2023 16:29:45 +0100 Subject: [PATCH] Implement semantic markup support for Ansible documentation in ansible-doc. (#80242) --- .../80242-ansible-doc-semantic-markup.yml | 2 + lib/ansible/cli/doc.py | 43 +++++++++++++++++++ .../testcol/plugins/modules/randommodule.py | 17 ++++---- .../ansible-doc/randommodule-text.output | 17 +++++--- .../targets/ansible-doc/randommodule.output | 13 +++--- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 changelogs/fragments/80242-ansible-doc-semantic-markup.yml diff --git a/changelogs/fragments/80242-ansible-doc-semantic-markup.yml b/changelogs/fragments/80242-ansible-doc-semantic-markup.yml new file mode 100644 index 00000000000..f98c2f9be59 --- /dev/null +++ b/changelogs/fragments/80242-ansible-doc-semantic-markup.yml @@ -0,0 +1,2 @@ +minor_changes: + - "ansible-doc - support semantic markup in text output (https://github.com/ansible/ansible/pull/80242)." diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index a43cb3307f6..dfe27f64b3e 100755 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -362,12 +362,23 @@ class DocCLI(CLI, RoleMixin): _ITALIC = re.compile(r"\bI\(([^)]+)\)") _BOLD = re.compile(r"\bB\(([^)]+)\)") _MODULE = re.compile(r"\bM\(([^)]+)\)") + _PLUGIN = re.compile(r"\bP\(([^#)]+)#([a-z]+)\)") _LINK = re.compile(r"\bL\(([^)]+), *([^)]+)\)") _URL = re.compile(r"\bU\(([^)]+)\)") _REF = re.compile(r"\bR\(([^)]+), *([^)]+)\)") _CONST = re.compile(r"\bC\(([^)]+)\)") + _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)" + _SEM_OPTION_NAME = re.compile(r"\bO" + _SEM_PARAMETER_STRING) + _SEM_OPTION_VALUE = re.compile(r"\bV" + _SEM_PARAMETER_STRING) + _SEM_ENV_VARIABLE = re.compile(r"\bE" + _SEM_PARAMETER_STRING) + _SEM_RET_VALUE = re.compile(r"\bRV" + _SEM_PARAMETER_STRING) _RULER = re.compile(r"\bHORIZONTALLINE\b") + # helper for unescaping + _UNESCAPE = re.compile(r"\\(.)") + _FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$') + _IGNORE_MARKER = 'ignore:' + # rst specific _RST_NOTE = re.compile(r".. note::") _RST_SEEALSO = re.compile(r".. seealso::") @@ -379,6 +390,33 @@ class DocCLI(CLI, RoleMixin): super(DocCLI, self).__init__(args) self.plugin_list = set() + @staticmethod + def _tty_ify_sem_simle(matcher): + text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1)) + return f"`{text}'" + + @staticmethod + def _tty_ify_sem_complex(matcher): + text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1)) + value = None + if '=' in text: + text, value = text.split('=', 1) + m = DocCLI._FQCN_TYPE_PREFIX_RE.match(text) + if m: + plugin_fqcn = m.group(1) + plugin_type = m.group(2) + text = m.group(3) + elif text.startswith(DocCLI._IGNORE_MARKER): + text = text[len(DocCLI._IGNORE_MARKER):] + plugin_fqcn = plugin_type = '' + else: + plugin_fqcn = plugin_type = '' + if value is not None: + text = f"{text}={value}" + if plugin_fqcn and plugin_type: + return f"`{text}' (of {plugin_type} {plugin_fqcn})" + return f"`{text}'" + @classmethod def find_plugins(cls, path, internal, plugin_type, coll_filter=None): display.deprecated("find_plugins method as it is incomplete/incorrect. use ansible.plugins.list functions instead.", version='2.17') @@ -393,8 +431,13 @@ class DocCLI(CLI, RoleMixin): t = cls._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word] t = cls._URL.sub(r"\1", t) # U(word) => word t = cls._LINK.sub(r"\1 <\2>", t) # L(word, url) => word + t = cls._PLUGIN.sub("[" + r"\1" + "]", t) # P(word#type) => [word] t = cls._REF.sub(r"\1", t) # R(word, sphinx-ref) => word t = cls._CONST.sub(r"`\1'", t) # C(word) => `word' + t = cls._SEM_OPTION_NAME.sub(cls._tty_ify_sem_complex, t) # O(expr) + t = cls._SEM_OPTION_VALUE.sub(cls._tty_ify_sem_simle, t) # V(expr) + t = cls._SEM_ENV_VARIABLE.sub(cls._tty_ify_sem_simle, t) # E(expr) + t = cls._SEM_RET_VALUE.sub(cls._tty_ify_sem_complex, t) # RV(expr) t = cls._RULER.sub("\n{0}\n".format("-" * 13), t) # HORIZONTALLINE => ------- # remove rst 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 f251a69f0ea..9d59a7bbf2a 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 @@ -3,7 +3,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: randommodule short_description: A random module @@ -18,22 +18,22 @@ deprecated: removed_in: '3.0.0' options: test: - description: Some text. + description: Some text. Consider not using O(ignore:foo=bar). type: str version_added: 1.2.0 sub: - description: Suboptions. + description: Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this. type: dict suboptions: subtest: - description: A suboption. + description: A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\foo\(1\).txt). type: int version_added: 1.1.0 # The following is the wrong syntax, and should not get processed # by add_collection_to_versions_and_dates() options: subtest2: - description: Another suboption. + description: Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\),d\\]). type: float version_added: 1.1.0 # The following is not supported in modules, and should not get processed @@ -53,7 +53,7 @@ extends_documentation_fragment: EXAMPLES = ''' ''' -RETURN = ''' +RETURN = r''' z_last: description: A last result. type: str @@ -63,7 +63,8 @@ z_last: m_middle: description: - This should be in the middle. - - Has some more data + - Has some more data. + - Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value). type: dict returned: success and 1st of month contains: @@ -74,7 +75,7 @@ m_middle: version_added: 1.4.0 a_first: - description: A first result. + description: A first result. Use RV(a_first=foo\(bar\\baz\)bam). type: str returned: success ''' diff --git a/test/integration/targets/ansible-doc/randommodule-text.output b/test/integration/targets/ansible-doc/randommodule-text.output index 51d7930a079..a9c426656d6 100644 --- a/test/integration/targets/ansible-doc/randommodule-text.output +++ b/test/integration/targets/ansible-doc/randommodule-text.output @@ -14,7 +14,8 @@ DEPRECATED: OPTIONS (= is mandatory): - sub - Suboptions. + Suboptions. Contains `sub.subtest', which can be set to `123'. + You can use `TEST_ENV' to set this. set_via: env: - deprecated: @@ -29,7 +30,8 @@ OPTIONS (= is mandatory): OPTIONS: - subtest2 - Another suboption. + Another suboption. Useful when [ansible.builtin.shuffle] + is used with value `[a,b,),d\]'. default: null type: float added in: version 1.1.0 @@ -39,14 +41,15 @@ OPTIONS (= is mandatory): SUBOPTIONS: - subtest - A suboption. + A suboption. Not compatible to `path=c:\foo(1).txt' (of + module ansible.builtin.copy). default: null type: int added in: version 1.1.0 of testns.testcol - test - Some text. + Some text. Consider not using `foo=bar'. default: null type: str added in: version 1.2.0 of testns.testcol @@ -74,13 +77,15 @@ EXAMPLES: RETURN VALUES: - a_first - A first result. + A first result. Use `a_first=foo(bar\baz)bam'. returned: success type: str - m_middle This should be in the middle. - Has some more data + Has some more data. + Check out `m_middle.suboption' and compare it to `a_first=foo' + and `value' (of lookup community.general.foo). returned: success and 1st of month type: dict diff --git a/test/integration/targets/ansible-doc/randommodule.output b/test/integration/targets/ansible-doc/randommodule.output index 25f46c36222..c882d4b3683 100644 --- a/test/integration/targets/ansible-doc/randommodule.output +++ b/test/integration/targets/ansible-doc/randommodule.output @@ -19,7 +19,7 @@ "module": "randommodule", "options": { "sub": { - "description": "Suboptions.", + "description": "Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this.", "env": [ { "deprecated": { @@ -34,14 +34,14 @@ ], "options": { "subtest2": { - "description": "Another suboption.", + "description": "Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\\),d\\\\]).", "type": "float", "version_added": "1.1.0" } }, "suboptions": { "subtest": { - "description": "A suboption.", + "description": "A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\\\foo\\(1\\).txt).", "type": "int", "version_added": "1.1.0", "version_added_collection": "testns.testcol" @@ -50,7 +50,7 @@ "type": "dict" }, "test": { - "description": "Some text.", + "description": "Some text. Consider not using O(ignore:foo=bar).", "type": "str", "version_added": "1.2.0", "version_added_collection": "testns.testcol" @@ -78,7 +78,7 @@ "metadata": null, "return": { "a_first": { - "description": "A first result.", + "description": "A first result. Use RV(a_first=foo\\(bar\\\\baz\\)bam).", "returned": "success", "type": "str" }, @@ -98,7 +98,8 @@ }, "description": [ "This should be in the middle.", - "Has some more data" + "Has some more data.", + "Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value)." ], "returned": "success and 1st of month", "type": "dict"