Skip to content
Merged
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
3 changes: 3 additions & 0 deletions changelogs/fragments/430-refs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- "Add new RST roles ``:ansoptref:`` and ``:ansretvalref:`` which allow to reference options and return values with explicit titles
(https://github.com/ansible-community/antsibull-docs/pull/430)."
26 changes: 25 additions & 1 deletion docs/collection-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ $ antsibull-docs lint-collection-docs --plugin-docs .
This subcommand has multiple options which allow to control validation. The most important options are:

* `--plugin-docs`: whether to validate schemas and markup of modules, plugins, and roles included in the collection. By default, this is not run (for backwards compatibility). We recommend to always specify this.
* `--check-extra-docs-refs`: whether references in `:anscollection:`, `:ansplugin`, `:ansopt:`, `:ansretval:` roles used in extra documentation should be checked.
* `--check-extra-docs-refs`: whether references in `:anscollection:`, `:ansplugin`, `:ansopt:`, `:ansoptref:`, `:ansretval:`, `:ansretvalref:` roles used in extra documentation should be checked.
* `--validate-collection-refs {self,dependent,all}`: Specify how to validate inter-plugin/module/role and inter-collection references in plugin/module/role documentation (if `--plugin-docs` is specified) and extra docs (if `--check-extra-docs-refs` is specified`). This covers Ansible markup, like `M(foo.bar.baz)` or `O(foo.bar.baz#module:parameter=value)`, and other links such as `seealso` sections. If set to `self`, only references to the same collection are validated. If set to `dependent`, only references to the collection itself and collections it (transitively) depends on are validated, including references to ansible-core (as `ansible.builtin`). If set to `all`, all references to other collections are validated.

If collections are referenced that are not installed and that are in scope, references to them will not be reported. Reporting these can be enabled by specifying `--disallow-unknown-collection-refs`.
Expand Down Expand Up @@ -260,6 +260,30 @@ Antsibull-docs provides several roles to reference Ansible content without havin

Basically the syntax is identical to the one of `:ansopt:`, except that this references return values instead of options.

* `:ansoptref:`: reference options of a module, plugin, or role.

The syntax is as follows:
```rst
An option with an option value, referencing an option of a plugin
(specified by its FQCN) and plugin type (module, lookup, filter, ...):
:ansoptref:`Title <namespace.name.plugin_name#plugin_type:option_name>`

For roles (plugin type "role"), you also have to specify the entrypoint
(usually "main"):
:ansoptref:`Title <namespace.name.role_name#role:entrypoint:option_name>`

Suboptions must be referenced by separating the different levels by dot:
:ansoptref:`Title <namespace.name.plugin#type:option.suboption.subsuboption=foo>`

You can use "[]" (with possible content) to indicate lists
(these are ignored and not shown anywhere):
:ansoptref:`Title <namespace.name.plugin#type:option[].suboption[n-1].subsuboption["key"]>`
```

* `:ansretvalref:`: format return values; reference return values of a module or plugin.

Basically the syntax is identical to the one of `:ansoptref:`, except that this references return values instead of options.

* `:ansenvvar:`: format environment variables with possible assignment.

The syntax is as follows:
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ source = [

[tool.mypy]
mypy_path = "stubs/"
# check_untyped_defs = true
# disallow_untyped_defs = true
# strict = true -- only try to enable once everything (including dependencies!) is typed
strict_equality = true
strict_bytes = true
warn_redundant_casts = true
# warn_return_any = true
# warn_unreachable = true

[[tool.mypy.overrides]]
module = "semantic_version"
Expand Down
45 changes: 45 additions & 0 deletions src/antsibull_docs/lint_extra_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
from .markup.semantic_helper import (
parse_collection_name,
parse_option,
parse_option_ref,
parse_plugin_name,
parse_return_value,
parse_return_value_ref,
)
from .rstcheck import check_rst_content

Expand All @@ -61,6 +63,20 @@ def _validate_option(value: str, names_linter: CollectionNameLinter) -> None:
raise ValueError(error)


def _validate_option_ref(value: str, names_linter: CollectionNameLinter) -> None:
target, _title = extract_explicit_title(value, require_title=True)
plugin_fqcn, plugin_type, entrypoint, option_link, option = parse_option_ref(target)
plugin = Plugin(
plugin_fqcn=plugin_fqcn,
plugin_type=plugin_type,
role_entrypoint=entrypoint,
)
for error in names_linter.validate_option_name(
plugin, option, option_link.split(".")
):
raise ValueError(error)


def _validate_return_value(value: str, names_linter: CollectionNameLinter) -> None:
plugin_fqcn, plugin_type, entrypoint, rv_link, rv, _ = parse_return_value(
value, "", "", require_plugin=False
Expand All @@ -76,6 +92,18 @@ def _validate_return_value(value: str, names_linter: CollectionNameLinter) -> No
raise ValueError(error)


def _validate_return_value_ref(value: str, names_linter: CollectionNameLinter) -> None:
target, _title = extract_explicit_title(value, require_title=True)
plugin_fqcn, plugin_type, entrypoint, rv_link, rv = parse_return_value_ref(target)
plugin = Plugin(
plugin_fqcn=plugin_fqcn,
plugin_type=plugin_type,
role_entrypoint=entrypoint,
)
for error in names_linter.validate_return_value(plugin, rv, rv_link.split(".")):
raise ValueError(error)


def _validate_plugin(value: str, names_linter: CollectionNameLinter) -> None:
plugin_fqcn, plugin_type, entrypoint = parse_plugin_name(value)
for error in names_linter.validate_plugin_fqcn(
Expand Down Expand Up @@ -116,10 +144,25 @@ def wrap(
def option_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return wrap(_docutils_unescape(text), rawtext, lineno, _validate_option)

# pylint:disable-next=unused-argument,dangerous-default-value
def option_ref_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return wrap(text, rawtext, lineno, _validate_option_ref)

# pylint:disable-next=unused-argument,dangerous-default-value
def return_value_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return wrap(_docutils_unescape(text), rawtext, lineno, _validate_return_value)

def return_value_ref_role( # pylint:disable=dangerous-default-value
name, # pylint:disable=unused-argument
rawtext,
text,
lineno,
inliner, # pylint:disable=unused-argument
options={}, # pylint:disable=unused-argument
content=[], # pylint:disable=unused-argument
):
return wrap(text, rawtext, lineno, _validate_return_value_ref)

# pylint:disable-next=unused-argument,dangerous-default-value
def plugin_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
target, _ = extract_explicit_title(text)
Expand All @@ -132,7 +175,9 @@ def collection_role(name, rawtext, text, lineno, inliner, options={}, content=[]

return {
"ansopt": option_role,
"ansoptref": option_ref_role,
"ansretval": return_value_role,
"ansretvalref": return_value_ref_role,
"ansplugin": plugin_role,
"anscollection": collection_role,
}
Expand Down
75 changes: 72 additions & 3 deletions src/antsibull_docs/markup/semantic_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import re
import typing as t

_ARRAY_STUB_RE = re.compile(r"\[([^\]]*)\]")
_ARRAY_STUB_SEP_START_RE = re.compile(r"[\[.]")
Expand All @@ -23,19 +24,57 @@ def _remove_array_stubs(text: str) -> str:
return _ARRAY_STUB_RE.sub("", text)


@t.overload
def _parse(
text: str,
plugin_fqcn: str | None,
plugin_type: str | None,
what: str,
require_plugin=False,
require_plugin: t.Literal[True],
*,
extract_value: t.Literal[False],
) -> tuple[str, str, str | None, str, str, None]: ...


@t.overload
def _parse(
text: str,
plugin_fqcn: str | None,
plugin_type: str | None,
what: str,
require_plugin: t.Literal[True],
*,
extract_value: bool = True,
) -> tuple[str, str, str | None, str, str, str | None]: ...


@t.overload
def _parse(
text: str,
plugin_fqcn: str | None,
plugin_type: str | None,
what: str,
require_plugin: bool = False,
*,
extract_value: bool = True,
) -> tuple[str | None, str | None, str | None, str, str, str | None]: ...


def _parse(
text: str,
plugin_fqcn: str | None,
plugin_type: str | None,
what: str,
require_plugin: bool = False,
*,
extract_value: bool = True,
) -> tuple[str | None, str | None, str | None, str, str, str | None]:
"""
Given the contents of O(...) / :ansopt:`...` with potential escaping removed,
split it into plugin FQCN, plugin type, option link name, option name, and option value.
"""
value = None
if "=" in text:
value: str | None = None
if extract_value and "=" in text:
text, value = text.split("=", 1)
m = _FQCN_TYPE_PREFIX_RE.match(text)
if m:
Expand Down Expand Up @@ -73,6 +112,21 @@ def parse_option(
)


def parse_option_ref(target: str) -> tuple[str, str, str | None, str, str]:
"""
Given the target of :ansoptref:`... <...>`, split it into plugin FQCN, plugin type,
entrypoint, option link name, and option name.
"""
return _parse(
target,
None,
None,
"option name",
require_plugin=True,
extract_value=False,
)[:5]


def parse_return_value(
text: str, plugin_fqcn: str | None, plugin_type: str | None, require_plugin=False
) -> tuple[str | None, str | None, str | None, str, str, str | None]:
Expand All @@ -90,6 +144,21 @@ def parse_return_value(
)


def parse_return_value_ref(target: str) -> tuple[str, str, str | None, str, str]:
"""
Given the target of :ansretvalref:`... <...>`, split it into plugin FQCN, plugin type,
entrypoint, return value link name, and return value name.
"""
return _parse(
target,
None,
None,
"return value name",
require_plugin=True,
extract_value=False,
)[:5]


def split_option_like_name(name: str) -> list[tuple[str, str | None]]:
"""
Given an option/return value name, splits it up into components separated by ``.``,
Expand Down
Loading