Skip to content

Commit

Permalink
Convert into two docs parsing backends. Make doc parsing backend conf…
Browse files Browse the repository at this point in the history
…igurable.
  • Loading branch information
felixfontein committed Nov 2, 2020
1 parent 1b7e2f3 commit 629a102
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 124 deletions.
1 change: 1 addition & 0 deletions antsibull.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ process_max = none
pypi_url = https://pypi.org/
thread_max = 80
max_retries = 10
doc_parsing_backend = ansible-doc
logging_cfg = {
version = 1.0
outputs = {
Expand Down
4 changes: 3 additions & 1 deletion antsibull/app_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def run(args):
_FIELDS_IN_APP_CTX = frozenset(('galaxy_url', 'logging_cfg', 'pypi_url'))

#: Field names in the args and config which whose value will be added to the lib_ctx
_FIELDS_IN_LIB_CTX = frozenset(('chunksize', 'process_max', 'thread_max', 'max_retries'))
_FIELDS_IN_LIB_CTX = frozenset(
('chunksize', 'process_max', 'thread_max', 'max_retries', 'doc_parsing_backend'))

#: lib_ctx should be restricted to things which do not belong in the API but an application or
#: user might want to tweak. Global, internal, incidental values are good to store here. Things
Expand Down Expand Up @@ -242,6 +243,7 @@ class LibContext(BaseModel):
process_max: t.Optional[int] = None
thread_max: int = 64
max_retries: int = 10
doc_parsing_backend: str = 'ansible-doc'

@p.validator('process_max', pre=True)
def convert_to_none(cls, value):
Expand Down
10 changes: 3 additions & 7 deletions antsibull/cli/doc_commands/stable.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ...collections import install_together
from ...compat import asyncio_run, best_get_loop
from ...dependency_files import DepsFile
from ...docs_parsing.ansible_doc import get_ansible_plugin_info, get_ansible_plugin_info_2
from ...docs_parsing.parsing import get_ansible_plugin_info
from ...docs_parsing.fqcn import get_fqcn_parts
from ...galaxy import CollectionDownloader
from ...logging import log
Expand Down Expand Up @@ -238,12 +238,8 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
"""

# Get the info from the plugins
if False:
plugin_info = asyncio_run(get_ansible_plugin_info(
venv, collection_dir, collection_names=collection_names))
else:
plugin_info = asyncio_run(get_ansible_plugin_info_2(
venv, collection_dir, collection_names=collection_names))
plugin_info = asyncio_run(get_ansible_plugin_info(
venv, collection_dir, collection_names=collection_names))
flog.notice('Finished parsing info from plugins')
# flog.fields(plugin_info=plugin_info).debug('Plugin data')

Expand Down
4 changes: 4 additions & 0 deletions antsibull/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#: Valid choices for a logging level field
LEVEL_CHOICES_F = p.Field(..., regex='^(CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG|DISABLED)$')

#: Valid choices for a logging level field
DOC_PARSING_BACKEND_CHOICES_F = p.Field('ansible-doc', regex='^(ansible-doc|ansible-internal)$')

#: Valid choice of the logging version field
VERSION_CHOICES_F = p.Field(..., regex=r'1\.0')

Expand Down Expand Up @@ -131,6 +134,7 @@ class ConfigModel(BaseModel):
pypi_url: p.HttpUrl = 'https://pypi.org/'
thread_max: int = 80
max_retries: int = 10
doc_parsing_backend: str = DOC_PARSING_BACKEND_CHOICES_F

@p.validator('process_max', pre=True)
def convert_to_none(cls, value):
Expand Down
64 changes: 64 additions & 0 deletions antsibull/docs_parsing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Author: Toshio Kuratomi <tkuratom@redhat.com>
# License: GPLv3+
# Copyright: Ansible Project, 2020
"""Parse documentation from ansible plugins using anible-doc."""

import os
import typing as t


#: Clear Ansible environment variables that set paths where plugins could be found.
ANSIBLE_PATH_ENVIRON: t.Dict[str, str] = os.environ.copy()
ANSIBLE_PATH_ENVIRON.update({'ANSIBLE_COLLECTIONS_PATH': '/dev/null',
'ANSIBLE_ACTION_PLUGINS': '/dev/null',
'ANSIBLE_CACHE_PLUGINS': '/dev/null',
'ANSIBLE_CALLBACK_PLUGINS': '/dev/null',
'ANSIBLE_CLICONF_PLUGINS': '/dev/null',
'ANSIBLE_CONNECTION_PLUGINS': '/dev/null',
'ANSIBLE_FILTER_PLUGINS': '/dev/null',
'ANSIBLE_HTTPAPI_PLUGINS': '/dev/null',
'ANSIBLE_INVENTORY_PLUGINS': '/dev/null',
'ANSIBLE_LOOKUP_PLUGINS': '/dev/null',
'ANSIBLE_LIBRARY': '/dev/null',
'ANSIBLE_MODULE_UTILS': '/dev/null',
'ANSIBLE_NETCONF_PLUGINS': '/dev/null',
'ANSIBLE_ROLES_PATH': '/dev/null',
'ANSIBLE_STRATEGY_PLUGINS': '/dev/null',
'ANSIBLE_TERMINAL_PLUGINS': '/dev/null',
'ANSIBLE_TEST_PLUGINS': '/dev/null',
'ANSIBLE_VARS_PLUGINS': '/dev/null',
'ANSIBLE_DOC_FRAGMENT_PLUGINS': '/dev/null',
})
try:
del ANSIBLE_PATH_ENVIRON['PYTHONPATH']
except KeyError:
# We just wanted to make sure there was no PYTHONPATH set...
# all python libs will come from the venv
pass
try:
del ANSIBLE_PATH_ENVIRON['ANSIBLE_COLLECTIONS_PATHS']
except KeyError:
# ANSIBLE_COLLECTIONS_PATHS is the deprecated name replaced by
# ANSIBLE_COLLECTIONS_PATH
pass


class ParsingError(Exception):
"""Error raised while parsing plugins for documentation."""


def _get_environment(collection_dir: t.Optional[str]) -> t.Dict[str, str]:
env = ANSIBLE_PATH_ENVIRON.copy()
if collection_dir is not None:
env['ANSIBLE_COLLECTIONS_PATH'] = collection_dir
else:
# Copy ANSIBLE_COLLECTIONS_PATH and ANSIBLE_COLLECTIONS_PATHS from the
# original environment.
for env_var in ('ANSIBLE_COLLECTIONS_PATH', 'ANSIBLE_COLLECTIONS_PATHS'):
try:
del env[env_var]
except KeyError:
pass
if env_var in os.environ:
env[env_var] = os.environ[env_var]
return env
120 changes: 4 additions & 116 deletions antsibull/docs_parsing/ansible_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

import asyncio
import json
import os
import sys
import tempfile
import traceback
import typing as t
from concurrent.futures import ThreadPoolExecutor
Expand All @@ -18,55 +16,16 @@
from ..compat import best_get_loop, create_task
from ..constants import DOCUMENTABLE_PLUGINS
from ..logging import log
from ..utils.get_pkg_data import get_antsibull_data
from ..vendored.json_utils import _filter_non_json_lines
from .fqcn import get_fqcn_parts
from . import _get_environment, ParsingError

if t.TYPE_CHECKING:
from ..venv import VenvRunner, FakeVenvRunner


mlog = log.fields(mod=__name__)

#: Clear Ansible environment variables that set paths where plugins could be found.
ANSIBLE_PATH_ENVIRON: t.Dict[str, str] = os.environ.copy()
ANSIBLE_PATH_ENVIRON.update({'ANSIBLE_COLLECTIONS_PATH': '/dev/null',
'ANSIBLE_ACTION_PLUGINS': '/dev/null',
'ANSIBLE_CACHE_PLUGINS': '/dev/null',
'ANSIBLE_CALLBACK_PLUGINS': '/dev/null',
'ANSIBLE_CLICONF_PLUGINS': '/dev/null',
'ANSIBLE_CONNECTION_PLUGINS': '/dev/null',
'ANSIBLE_FILTER_PLUGINS': '/dev/null',
'ANSIBLE_HTTPAPI_PLUGINS': '/dev/null',
'ANSIBLE_INVENTORY_PLUGINS': '/dev/null',
'ANSIBLE_LOOKUP_PLUGINS': '/dev/null',
'ANSIBLE_LIBRARY': '/dev/null',
'ANSIBLE_MODULE_UTILS': '/dev/null',
'ANSIBLE_NETCONF_PLUGINS': '/dev/null',
'ANSIBLE_ROLES_PATH': '/dev/null',
'ANSIBLE_STRATEGY_PLUGINS': '/dev/null',
'ANSIBLE_TERMINAL_PLUGINS': '/dev/null',
'ANSIBLE_TEST_PLUGINS': '/dev/null',
'ANSIBLE_VARS_PLUGINS': '/dev/null',
'ANSIBLE_DOC_FRAGMENT_PLUGINS': '/dev/null',
})
try:
del ANSIBLE_PATH_ENVIRON['PYTHONPATH']
except KeyError:
# We just wanted to make sure there was no PYTHONPATH set...
# all python libs will come from the venv
pass
try:
del ANSIBLE_PATH_ENVIRON['ANSIBLE_COLLECTIONS_PATHS']
except KeyError:
# ANSIBLE_COLLECTIONS_PATHS is the deprecated name replaced by
# ANSIBLE_COLLECTIONS_PATH
pass


class ParsingError(Exception):
"""Error raised while parsing plugins for documentation."""


def _process_plugin_results(plugin_type: str,
plugin_names: t.Iterable[str],
Expand Down Expand Up @@ -199,23 +158,6 @@ async def _get_plugin_info(plugin_type: str, ansible_doc: 'sh.Command',
return results


def _get_environment(collection_dir: t.Optional[str]) -> t.Dict[str, str]:
env = ANSIBLE_PATH_ENVIRON.copy()
if collection_dir is not None:
env['ANSIBLE_COLLECTIONS_PATH'] = collection_dir
else:
# Copy ANSIBLE_COLLECTIONS_PATH and ANSIBLE_COLLECTIONS_PATHS from the
# original environment.
for env_var in ('ANSIBLE_COLLECTIONS_PATH', 'ANSIBLE_COLLECTIONS_PATHS'):
try:
del env[env_var]
except KeyError:
pass
if env_var in os.environ:
env[env_var] = os.environ[env_var]
return env


async def get_ansible_plugin_info(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
collection_dir: t.Optional[str],
collection_names: t.Optional[t.List[str]] = None
Expand All @@ -236,6 +178,9 @@ async def get_ansible_plugin_info(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
{information from ansible-doc --json. See the ansible-doc documentation for more
info.}
"""
flog = mlog.fields(func='get_ansible_plugin_info')
flog.debug('Enter')

env = _get_environment(collection_dir)

# Setup an sh.Command to run ansible-doc from the venv with only the collections we
Expand Down Expand Up @@ -301,62 +246,5 @@ async def get_ansible_plugin_info(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
# done so, we want to then fail by raising one of the exceptions.
raise ParsingError('Parsing of plugins failed')

return plugin_map


async def get_ansible_plugin_info_2(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
collection_dir: t.Optional[str],
collection_names: t.Optional[t.List[str]] = None
) -> t.Dict[str, t.Dict[str, t.Any]]:
"""
Retrieve information about all of the Ansible Plugins.
:arg venv: A VenvRunner into which Ansible has been installed.
:arg collection_dir: Directory in which the collections have been installed.
If ``None``, the collections are assumed to be in the current
search path for Ansible.
:arg collection_names: Optional list of collections. If specified, will only collect
information for plugins in these collections.
:returns: A nested directory structure that looks like::
plugin_type:
plugin_name: # Includes namespace and collection.
{information from ansible-doc --json. See the ansible-doc documentation for more
info.}
"""
flog = mlog.fields(func='get_ansible_plugin_info_2')
flog.debug('Enter')

env = _get_environment(collection_dir)

venv_python = venv.get_command('python')

with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(get_antsibull_data('collection-enum.py'))
collection_enum_args = [tmp_file.name]
if collection_names and len(collection_names) == 1:
# Script allows to filter by one collection
collection_enum_args.append(collection_names[0])
collection_enum_cmd = venv_python(*collection_enum_args, _env=env)
raw_result = collection_enum_cmd.stdout.decode('utf-8', errors='surrogateescape')
result = json.loads(_filter_non_json_lines(raw_result)[0])
del raw_result
del collection_enum_cmd

plugin_map = {}
for plugin_type, plugins in result['plugins'].items():
plugin_map[plugin_type] = {}
for plugin_name, plugin_data in plugins.items():
if '.' not in plugin_name:
plugin_name = 'ansible.builtin.{0}'.format(plugin_name)
if 'ansible-doc' in plugin_data:
plugin_map[plugin_type][plugin_name] = plugin_data['ansible-doc']
else:
plugin_log = flog.fields(plugin_type=plugin_type, plugin_name=plugin_name)
plugin_log.fields(error=plugin_data['error']).error(
'Error while extracting documentation. Will not document this plugin.')

# TODO: use result['collections']

flog.debug('Leave')
return plugin_map
78 changes: 78 additions & 0 deletions antsibull/docs_parsing/ansible_internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Author: Felix Fontein <felix@fontein.de>
# Author: Toshio Kuratomi <tkuratom@redhat.com>
# License: GPLv3+
# Copyright: Ansible Project, 2020
"""Parse documentation from ansible plugins using anible-doc."""

import json
import tempfile
import typing as t

from ..logging import log
from ..utils.get_pkg_data import get_antsibull_data
from ..vendored.json_utils import _filter_non_json_lines
from . import _get_environment

if t.TYPE_CHECKING:
from ..venv import VenvRunner, FakeVenvRunner


mlog = log.fields(mod=__name__)


async def get_ansible_plugin_info(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
collection_dir: t.Optional[str],
collection_names: t.Optional[t.List[str]] = None
) -> t.Dict[str, t.Dict[str, t.Any]]:
"""
Retrieve information about all of the Ansible Plugins.
:arg venv: A VenvRunner into which Ansible has been installed.
:arg collection_dir: Directory in which the collections have been installed.
If ``None``, the collections are assumed to be in the current
search path for Ansible.
:arg collection_names: Optional list of collections. If specified, will only collect
information for plugins in these collections.
:returns: A nested directory structure that looks like::
plugin_type:
plugin_name: # Includes namespace and collection.
{information from ansible-doc --json. See the ansible-doc documentation for more
info.}
"""
flog = mlog.fields(func='get_ansible_plugin_info')
flog.debug('Enter')

env = _get_environment(collection_dir)

venv_python = venv.get_command('python')

with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(get_antsibull_data('collection-enum.py'))
collection_enum_args = [tmp_file.name]
if collection_names and len(collection_names) == 1:
# Script allows to filter by one collection
collection_enum_args.append(collection_names[0])
collection_enum_cmd = venv_python(*collection_enum_args, _env=env)
raw_result = collection_enum_cmd.stdout.decode('utf-8', errors='surrogateescape')
result = json.loads(_filter_non_json_lines(raw_result)[0])
del raw_result
del collection_enum_cmd

plugin_map = {}
for plugin_type, plugins in result['plugins'].items():
plugin_map[plugin_type] = {}
for plugin_name, plugin_data in plugins.items():
if '.' not in plugin_name:
plugin_name = 'ansible.builtin.{0}'.format(plugin_name)
if 'ansible-doc' in plugin_data:
plugin_map[plugin_type][plugin_name] = plugin_data['ansible-doc']
else:
plugin_log = flog.fields(plugin_type=plugin_type, plugin_name=plugin_name)
plugin_log.fields(error=plugin_data['error']).error(
'Error while extracting documentation. Will not document this plugin.')

# TODO: use result['collections']

flog.debug('Leave')
return plugin_map
Loading

0 comments on commit 629a102

Please sign in to comment.