Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow wildcards for collections subcommand when --use-current is used #219

Merged
merged 1 commit into from
Sep 27, 2023
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
2 changes: 2 additions & 0 deletions changelogs/fragments/219-collection-wildcards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "Allow specifying wildcards for the collection names for the ``collections`` subcommand if ``--use-current`` is specified (https://github.com/ansible-community/antsibull-docs/pull/219)."
12 changes: 10 additions & 2 deletions src/antsibull_docs/cli/antsibull_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
import antsibull_docs # noqa: E402

from ..constants import DOCUMENTABLE_PLUGINS # noqa: E402
from ..docs_parsing.fqcn import is_collection_name, is_fqcn # noqa: E402
from ..docs_parsing.fqcn import ( # noqa: E402
is_collection_name,
is_fqcn,
is_wildcard_collection_name,
)
from ..schemas.app_context import DocsAppContext # noqa: E402
from .doc_commands import ( # noqa: E402
collection,
Expand Down Expand Up @@ -154,6 +158,8 @@ def _normalize_collection_options(args: argparse.Namespace) -> None:

for collection_name in args.collections:
if not is_collection_name(collection_name):
if args.use_current and is_wildcard_collection_name(collection_name):
continue
raise InvalidArgumentError(
f"The collection, {collection_name}, is not a valid collection name."
)
Expand Down Expand Up @@ -444,7 +450,9 @@ def parse_args(program_name: str, args: list[str]) -> argparse.Namespace:
dest="collections",
help="One or more collections to document. No paths or URLs are"
" supported. Collections are assumed to exist on Galaxy, or be"
" installed locally when --use-current is used.",
" installed locally when --use-current is used. When --use-current"
" is used, the wildcard '*' can be used for the namespace, the"
" collection name, or both ('foo.*', '*.bar', '*.*').",
)

#
Expand Down
5 changes: 4 additions & 1 deletion src/antsibull_docs/cli/doc_commands/_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ def generate_docs_for_all_collections( # noqa: C901

_validate_options(collection_names, exclude_collection_names, use_html_blobs)

if collection_names is not None and "ansible.builtin" not in collection_names:
if collection_names is not None and all(
ab not in collection_names
for ab in ("ansible.builtin", "ansible.*", "*.builtin", "*.*")
):
exclude_collection_names = ["ansible.builtin"]

app_ctx = app_context.app_ctx.get()
Expand Down
72 changes: 68 additions & 4 deletions src/antsibull_docs/docs_parsing/ansible_doc_core_213.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from ..constants import DOCUMENTABLE_PLUGINS
from . import AnsibleCollectionMetadata, _get_environment
from .ansible_doc import get_collection_metadata
from .fqcn import get_fqcn_parts
from .fqcn import get_fqcn_parts, is_collection_name, is_wildcard_collection_name

if t.TYPE_CHECKING:
from antsibull_core.venv import FakeVenvRunner, VenvRunner
Expand Down Expand Up @@ -82,6 +82,57 @@ def _get_ansible_doc_filters(
return []


def _get_matcher(wildcard: str) -> t.Callable[[str], bool]:
namespace, collection = wildcard.split(".", 1)

if namespace == "*":
if collection == "*":
return lambda collection_name: True
postfix = f".{collection}"
return lambda collection_name: collection_name.endswith(postfix)

if collection == "*":
prefix = f"{namespace}."
return lambda collection_name: collection_name.startswith(prefix)

return lambda collection_name: collection_name == wildcard


def _limit_by_wildcards(
collection_metadata: dict[str, AnsibleCollectionMetadata],
collection_names: list[str],
) -> tuple[dict[str, AnsibleCollectionMetadata], list[str]]:
wildcard_matchers: dict[str, t.Callable[[str], bool]] = {}
wildcard_counters: dict[str, int] = {}
for wildcard in collection_names:
wildcard_matchers[wildcard] = _get_matcher(wildcard)
wildcard_counters[wildcard] = 0

ret_collection_metadata = {}
ret_collection_names = []

for collection_name, collection_data in collection_metadata.items():
found = False
for wildcard, matcher in wildcard_matchers.items():
if matcher(collection_name):
wildcard_counters[wildcard] += 1
found = True
if not found:
if collection_name == "ansible.builtin":
ret_collection_metadata[collection_name] = collection_data
continue
ret_collection_metadata[collection_name] = collection_data
ret_collection_names.append(collection_name)

for wildcard, count in wildcard_counters.items():
if count == 0:
raise RuntimeError(
f"{wildcard} does not match any of the collections"
f" in {', '.join(sorted(collection_metadata))}"
)
return ret_collection_metadata, ret_collection_names


async def get_ansible_plugin_info(
venv: VenvRunner | FakeVenvRunner,
ansible_core_version: PypiVer,
Expand Down Expand Up @@ -121,14 +172,27 @@ async def get_ansible_plugin_info(
collection_dir, keep_current_collections_path=fetch_all_installed, venv=venv
)

has_wildcards = collection_names is not None and any(
is_wildcard_collection_name(cn) and not is_collection_name(cn)
for cn in collection_names
)

flog.debug("Retrieving collection metadata")
collection_metadata = await get_collection_metadata(
venv, env, None if has_wildcards else collection_names
)

if has_wildcards:
flog.debug("Restricting collection list by wildcards")
collection_metadata, collection_names = _limit_by_wildcards(
collection_metadata, collection_names or []
)

flog.debug("Retrieving and loading plugin documentation")
ansible_doc_output = await _call_ansible_doc(
venv, env, *_get_ansible_doc_filters(ansible_core_version, collection_names)
)

flog.debug("Retrieving collection metadata")
collection_metadata = await get_collection_metadata(venv, env, collection_names)

flog.debug("Processing plugin documentation")
plugin_map: MutableMapping[str, MutableMapping[str, t.Any]] = {}
for plugin_type in DOCUMENTABLE_PLUGINS:
Expand Down
15 changes: 15 additions & 0 deletions src/antsibull_docs/docs_parsing/fqcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
NAMESPACE_RE_STR = "[a-z0-9][a-z0-9_]+"
#: Format of a FQCN
COLLECTION_NAME_RE = re.compile(rf"^({NAMESPACE_RE_STR})\.({NAMESPACE_RE_STR})$")
COLLECTION_WILDCARD_RE = re.compile(
rf"^({NAMESPACE_RE_STR}|\*)\.({NAMESPACE_RE_STR}|\*)$"
)
FQCN_RE = re.compile(rf"^({NAMESPACE_RE_STR})\.({NAMESPACE_RE_STR})\.(.*)$")
FQCN_STRICT_RE = re.compile(
rf"^({NAMESPACE_RE_STR})\.({NAMESPACE_RE_STR})\.({NAMESPACE_RE_STR}(?:\.{NAMESPACE_RE_STR})*)$"
Expand Down Expand Up @@ -82,3 +85,15 @@ def is_collection_name(value: str) -> bool:
:returns: ``True`` if the value is a collection name, ``False`` if it is not.
"""
return bool(COLLECTION_NAME_RE.match(value))


def is_wildcard_collection_name(value: str) -> bool:
"""
Return whether ``value`` is a collection name, where the namespace or the name
can be a wildcard.

:arg value: The value to test.
:returns: ``True`` if the value is a collection name or wildcard, ``False``
if it is not.
"""
return bool(COLLECTION_WILDCARD_RE.match(value))