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

Use docs/docsite/links.yml file to create extra links #355

Merged
merged 5 commits into from
Mar 22, 2022
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/355-extra-links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- Allow collections to specify extra links (https://github.com/ansible-community/antsibull/pull/355).
2 changes: 2 additions & 0 deletions src/antsibull/cli/antsibull_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from ..args import get_toplevel_parser, normalize_toplevel_options
from ..lint_extra_docs import lint_collection_extra_docs_files
from ..collection_links import lint_collection_links


def run(args: List[str]) -> int:
Expand Down Expand Up @@ -114,6 +115,7 @@ def command_lint_collection_docs(args: Any) -> int:
:arg args: Parsed arguments
"""
errors = lint_collection_extra_docs_files(args.collection_root_path)
errors.extend(lint_collection_links(args.collection_root_path))

messages = sorted(set(f'{error[0]}:{error[1]}:{error[2]}: {error[3]}' for error in errors))

Expand Down
5 changes: 4 additions & 1 deletion src/antsibull/cli/doc_commands/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ... import app_context
from ...augment_docs import augment_docs
from ...compat import asyncio_run
from ...collection_links import CollectionLinks
from ...docs_parsing import AnsibleCollectionMetadata
from ...docs_parsing.fqcn import get_fqcn_parts, is_fqcn
from ...jinja2.environment import doc_environment
Expand Down Expand Up @@ -102,7 +103,9 @@ def generate_plugin_docs(plugin_type: str, plugin_name: str,
error_tmpl = env.get_template('plugin-error.rst.j2')

asyncio_run(write_plugin_rst(
collection_name, AnsibleCollectionMetadata.empty(), plugin, plugin_type,
collection_name,
AnsibleCollectionMetadata.empty(),
CollectionLinks(), plugin, plugin_type,
plugin_info, errors, plugin_tmpl, error_tmpl, '',
path_override=output_path,
use_html_blobs=app_ctx.use_html_blobs))
Expand Down
9 changes: 9 additions & 0 deletions src/antsibull/cli/doc_commands/stable.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ...compat import asyncio_run, best_get_loop
from ...dependency_files import DepsFile
from ...extra_docs import load_collections_extra_docs
from ...collection_links import load_collections_links
from ...docs_parsing.parsing import get_ansible_plugin_info
from ...docs_parsing.fqcn import get_fqcn_parts
from ...docs_parsing.routing import (
Expand Down Expand Up @@ -330,6 +331,11 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
{name: data.path for name, data in collection_metadata.items()}))
flog.debug('Finished getting collection extra docs data')

# Load collection extra docs data
link_data = asyncio_run(load_collections_links(
{name: data.path for name, data in collection_metadata.items()}))
flog.debug('Finished getting collection link data')

plugin_contents = get_plugin_contents(plugin_info, nonfatal_errors)
collection_to_plugin_info = get_collection_contents(plugin_contents)
# Make sure collections without documentable plugins are mentioned
Expand Down Expand Up @@ -363,17 +369,20 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
collection_metadata=collection_metadata,
squash_hierarchy=squash_hierarchy,
extra_docs_data=extra_docs_data,
link_data=link_data,
breadcrumbs=breadcrumbs))
flog.notice('Finished writing indexes')

asyncio_run(output_all_plugin_stub_rst(stubs_info, dest_dir,
collection_metadata=collection_metadata,
link_data=link_data,
squash_hierarchy=squash_hierarchy))
flog.debug('Finished writing plugin stubs')

asyncio_run(output_all_plugin_rst(collection_to_plugin_info, plugin_info,
nonfatal_errors, dest_dir,
collection_metadata=collection_metadata,
link_data=link_data,
squash_hierarchy=squash_hierarchy,
use_html_blobs=use_html_blobs))
flog.debug('Finished writing plugin docs')
Expand Down
233 changes: 233 additions & 0 deletions src/antsibull/collection_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# coding: utf-8
# Author: Felix Fontein <felix@fontein.de>
# License: GPLv3+
# Copyright: Ansible Project, 2021
"""Handle collection-specific links from galaxy.yml and docs/docsite/links.yml."""

import asyncio
import json
import os
import os.path
import typing as t

import asyncio_pool

from pydantic import Extra
from pydantic.error_wrappers import ValidationError, display_errors

from . import app_context
from .logging import log
from .yaml import load_yaml_file
from .schemas.collection_links import (
CollectionEditOnGitHub,
Link,
IRCChannel,
MatrixRoom,
MailingList,
Communication,
CollectionLinks,
)


mlog = log.fields(mod=__name__)


_ANSIBLE_CORE_METADATA = dict(
edit_on_github=dict(
repository='ansible/ansible',
branch='devel',
path_prefix='lib/ansible/',
),
authors=['Ansible, Inc.'],
description='These are all modules and plugins contained in ansible-core.',
links=[
dict(description='Issue Tracker', url='https://github.com/ansible/ansible/issues'),
dict(description='Repository (Sources)', url='https://github.com/ansible/ansible'),
],
communication=dict(
irc_channels=[dict(
topic='General usage and support questions',
network='Libera',
channel='#ansible',
)],
matrix_rooms=[dict(
topic='General usage and support questions',
room='#users:ansible.im',
)],
mailing_lists=[dict(
topic='Ansible Project List',
url='https://groups.google.com/g/ansible-project',
)],
),
)


def _extract_authors(data: t.Dict) -> t.List[str]:
authors = data.get('authors')
if not isinstance(authors, list):
return []

return [str(author) for author in authors]


def _extract_description(data: t.Dict) -> t.Optional[str]:
desc = data.get('description')
return desc if isinstance(desc, str) else None


def _extract_galaxy_links(data: t.Dict) -> t.List[Link]:
result = []

def extract(key: str, desc: str,
not_if_equals_one_of: t.Optional[t.List[str]] = None) -> None:
url = data.get(key)
if not_if_equals_one_of:
for other_key in not_if_equals_one_of:
if data.get(other_key) == url:
return
if isinstance(url, str):
result.append(Link.parse_obj(dict(description=desc, url=url)))

# extract('documentation', 'Documentation')
extract('issues', 'Issue Tracker')
extract('homepage', 'Homepage', not_if_equals_one_of=['documentation', 'repository'])
extract('repository', 'Repository (Sources)')
return result


def load(links_data: t.Optional[t.Dict], galaxy_data: t.Optional[t.Dict],
manifest_data: t.Optional[t.Dict]) -> CollectionLinks:
if links_data:
ld = links_data.copy()
# The authors and description field always comes from collection metadata
ld.pop('authors', None)
ld.pop('description', None)
# Links only comes in directly
ld.pop('links', None)
else:
ld = {}
try:
result = CollectionLinks.parse_obj(ld)
except ValidationError:
result = CollectionLinks.parse_obj({})

# Parse MANIFEST or galaxy data
if isinstance(manifest_data, dict):
collection_info = manifest_data.get('collection_info')
if isinstance(collection_info, dict):
result.authors = _extract_authors(collection_info)
result.description = _extract_description(collection_info)
result.links.extend(_extract_galaxy_links(collection_info))
elif isinstance(galaxy_data, dict):
result.authors = _extract_authors(galaxy_data)
result.description = _extract_description(galaxy_data)
result.links.extend(_extract_galaxy_links(galaxy_data))

result.links.extend(result.extra_links)

return result


async def load_collection_links(collection_name: str,
collection_path: str,
) -> CollectionLinks:
'''Given a collection name and path, load links data.

:arg collection_name: Dotted collection name.
:arg collection_path: Path to the collection.
:returns: A CollectionLinks instance.
'''
flog = mlog.fields(func='load_collection_links')
flog.debug('Enter')

if collection_name == 'ansible.builtin':
return CollectionLinks.parse_obj(_ANSIBLE_CORE_METADATA)

try:
# Load links data
index_path = os.path.join(collection_path, 'docs', 'docsite', 'links.yml')
links_data = None
if os.path.isfile(index_path):
links_data = load_yaml_file(index_path)

# Load galaxy.yml
galaxy_data = None
galaxy_path = os.path.join(collection_path, 'galaxy.yml')
if os.path.isfile(galaxy_path):
galaxy_data = load_yaml_file(galaxy_path)

# Load MANIFEST.json
manifest_data = None
manifest_path = os.path.join(collection_path, 'MANIFEST.json')
if os.path.isfile(manifest_path):
with open(manifest_path, 'rb') as f:
manifest_data = json.loads(f.read())

return load(links_data=links_data, galaxy_data=galaxy_data, manifest_data=manifest_data)
finally:
flog.debug('Leave')


async def load_collections_links(collection_paths: t.Mapping[str, str]
) -> t.Mapping[str, CollectionLinks]:
'''Load links data.

:arg collection_paths: Mapping of collection_name to the collection's path.
:returns: A mapping of collection_name to CollectionLinks.
'''
flog = mlog.fields(func='load_collections_links')
flog.debug('Enter')

loaders = {}
lib_ctx = app_context.lib_ctx.get()

async with asyncio_pool.AioPool(size=lib_ctx.thread_max) as pool:
for collection_name, collection_path in collection_paths.items():
loaders[collection_name] = await pool.spawn(
load_collection_links(collection_name, collection_path))

responses = await asyncio.gather(*loaders.values())

# Note: Python dicts have always had a stable order as long as you don't modify the dict.
# So loaders (implicitly, the keys) and responses have a matching order here.
result = dict(zip(loaders, responses))

flog.debug('Leave')
return result


def lint_collection_links(collection_path: str) -> t.List[t.Tuple[str, int, int, str]]:
'''Given a path, lint links data.

:arg collection_path: Path to the collection.
:returns: List of tuples (filename, row, column, error) indicating linting errors.
'''
flog = mlog.fields(func='lint_collection_links')
flog.debug('Enter')

result = []

for cls in (
CollectionEditOnGitHub, Link, IRCChannel, MatrixRoom, MailingList, Communication,
CollectionLinks,
):
cls.__config__.extra = Extra.forbid

try:
index_path = os.path.join(collection_path, 'docs', 'docsite', 'links.yml')
if not os.path.isfile(index_path):
return result

links_data = load_yaml_file(index_path)
for forbidden_key in ('authors', 'description', 'links'):
if forbidden_key in links_data:
result.append((index_path, 0, 0, f"The key '{forbidden_key}' must not be used"))
try:
CollectionLinks.parse_obj(links_data)
except ValidationError as exc:
for error in exc.errors():
result.append((index_path, 0, 0, display_errors([error]).replace('\n ', ':')))

return result
finally:
flog.debug('Leave')
3 changes: 3 additions & 0 deletions src/antsibull/data/docsite/plugin-error.rst.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.. Document meta section

:orphan:
{% if edit_on_github_url %}
:github_url: @{ edit_on_github_url }@?description=%23%23%23%23%23%20SUMMARY%0A%3C!---%20Your%20description%20here%20--%3E%0A%0A%0A%23%23%23%23%23%20ISSUE%20TYPE%0A-%20Docs%20Pull%20Request%0A%0A%2Blabel:%20docsite_pr
{% endif %}

.. Document body

Expand Down
28 changes: 21 additions & 7 deletions src/antsibull/data/docsite/plugin.rst.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
.. Document meta

:orphan:
{% if edit_on_github_url %}
:github_url: @{ edit_on_github_url }@?description=%23%23%23%23%23%20SUMMARY%0A%3C!---%20Your%20description%20here%20--%3E%0A%0A%0A%23%23%23%23%23%20ISSUE%20TYPE%0A-%20Docs%20Pull%20Request%0A%0A%2Blabel:%20docsite_pr
{% endif %}

.. |antsibull-internal-nbsp| unicode:: 0xA0
:trim:
Expand Down Expand Up @@ -309,17 +312,28 @@ Authors

{% endif %}

{% if collection == 'ansible.builtins' -%}
.. hint::
{% if plugin_type == 'module' %}
If you notice any issues in this documentation, you can `edit this document <https://github.com/ansible/ansible/edit/devel/lib/ansible/modules/@{ source }@?description=%23%23%23%23%23%20SUMMARY%0A%3C!---%20Your%20description%20here%20--%3E%0A%0A%0A%23%23%23%23%23%20ISSUE%20TYPE%0A-%20Docs%20Pull%20Request%0A%0A%2Blabel:%20docsite_pr>`_ to improve it.
{% else %}
If you notice any issues in this documentation, you can `edit this document <https://github.com/ansible/ansible/edit/devel/lib/ansible/plugins/@{ plugin_type }@/@{ source }@?description=%23%23%23%23%23%20SUMMARY%0A%3C!---%20Your%20description%20here%20--%3E%0A%0A%0A%23%23%23%23%23%20ISSUE%20TYPE%0A-%20Docs%20Pull%20Request%0A%0A%2Blabel:%20docsite_pr>`_ to improve it.

{% if plugin_type != 'module' %}
.. hint::
Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
{% endif %}

.. Extra links

{% if collection_links or not collection_communication.empty %}
Collection links
~~~~~~~~~~~~~~~~

.. raw:: html

<p class="ansible-links">
{% for link in collection_links %}
<a href="@{ link.url | escape }@" aria-role="button" target="_blank" rel="noopener external">@{ link.description | escape }@</a>
{% endfor %}
{% if not collection_communication.empty %}
{# WARNING: the anchor is created from Sphinx from the label and might change #}
<a href="./#communication-for-@{ collection | replace('.', '-') | escape }@" aria-role="button" target="_blank">Communication</a>
{% endif %}
</p>
{% endif %}

.. Parsing errors
Expand Down
Loading