azdev/operations/help/refdoc/common/directives.py (165 lines of code) (raw):
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import copy
import argparse
from os.path import expanduser
from unittest.mock import patch
from docutils import nodes
from docutils.statemachine import ViewList
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.util.compat import Directive
from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util.docfields import Field
from azure.cli.core import MainCommandsLoader, AzCli # pylint: disable=import-error
from azure.cli.core.commands import AzCliCommandInvoker # pylint: disable=import-error
from azure.cli.core.parser import AzCliCommandParser # pylint: disable=import-error
from azure.cli.core._help import AzCliHelp, CliCommandHelpFile, ArgumentGroupRegistry # pylint: disable=import-error
_USER_HOME = expanduser('~')
_CLI_FIELD_TYPES = [
Field('summary', label='Summary', has_arg=False,
names=('summary', 'shortdesc')),
Field('description', label='Description', has_arg=False,
names=('description', 'desc', 'longdesc'))
]
class CliBaseDirective(ObjectDescription):
def handle_signature(self, sig, signode):
signode += addnodes.desc_addname(sig, sig)
return sig
def needs_arglist(self):
return False
def add_target_and_index(self, name, sig, signode):
signode['ids'].append(name)
def get_index_text(self, modname, name): # pylint: disable=unused-argument
return name
class CliGroupDirective(CliBaseDirective):
doc_field_types = copy.copy(_CLI_FIELD_TYPES)
doc_field_types.extend([
Field('docsource', label='Doc Source', has_arg=False,
names=('docsource', 'documentsource')),
Field('deprecated', label='Deprecated', has_arg=False,
names=('deprecated',))
])
class CliCommandDirective(CliBaseDirective):
doc_field_types = copy.copy(_CLI_FIELD_TYPES)
doc_field_types.extend([
Field('docsource', label='Doc Source', has_arg=False,
names=('docsource', 'documentsource')),
Field('deprecated', label='Deprecated', has_arg=False,
names=('deprecated',))
])
class CliArgumentDirective(CliBaseDirective):
doc_field_types = copy.copy(_CLI_FIELD_TYPES)
doc_field_types.extend([
Field('required', label='Required', has_arg=False,
names=('required',)),
Field('values', label='Allowed values', has_arg=False,
names=('values', 'choices', 'options')),
Field('default', label='Default value', has_arg=False,
names=('default',)),
Field('source', label='Values from', has_arg=False,
names=('source', 'sources')),
Field('deprecated', label='Deprecated', has_arg=False,
names=('deprecated',))
])
class CliExampleDirective(CliBaseDirective):
pass
class AbstractHelpGenDirective(Directive):
""" Generic Sphinx Directive for generating azure cli documentation.
Should be overridden for core CLI or CLI extensions documentation
"""
_INDENT = ' ' * 3
_DOUBLE_INDENT = _INDENT * 2
def make_rst(self): # pylint: disable=too-many-statements, too-many-nested-blocks
az_cli = AzCli(cli_name='az',
commands_loader_cls=MainCommandsLoader,
invocation_cls=AzCliCommandInvoker,
parser_cls=AzCliCommandParser,
help_cls=AzCliHelp)
with patch('getpass.getuser', return_value='your_system_user_login_name'):
help_files = self._get_help_files(az_cli)
doc_source_map = self._load_doc_source_map()
group_registry = None
for help_file in help_files: # pylint: disable=too-many-nested-blocks
is_command = isinstance(help_file, CliCommandHelpFile)
# it is top level group az if command is empty
yield '.. cli{}:: {}'.format('command' if is_command else 'group',
help_file.command if help_file.command else 'az')
yield ''
yield '{}:summary: {}'.format(self._INDENT, help_file.short_summary)
yield '{}:description: {}'.format(self._INDENT, help_file.long_summary)
if help_file.deprecate_info:
yield '{}:deprecated: {}'.format(self._INDENT,
help_file.deprecate_info._get_message(help_file.deprecate_info)) # pylint: disable=protected-access
doc_source_content = self._get_doc_source_content(doc_source_map, help_file)
if doc_source_content:
yield doc_source_content
yield ''
if is_command and help_file.parameters:
group_registry = ArgumentGroupRegistry(
[p.group_name for p in help_file.parameters if p.group_name])
for arg in sorted(help_file.parameters, key=lambda p: group_registry.get_group_priority(p.group_name) + str(not p.required) + p.name): # pylint: disable=line-too-long, cell-var-from-loop
yield '{}.. cliarg:: {}'.format(self._INDENT, arg.name)
yield ''
yield '{}:required: {}'.format(self._DOUBLE_INDENT, arg.required)
if arg.deprecate_info:
yield '{}:deprecated: {}'.format(self._DOUBLE_INDENT,
arg.deprecate_info._get_message( # pylint: disable=protected-access
arg.deprecate_info))
short_summary = arg.short_summary or ''
possible_values_index = short_summary.find(' Possible values include')
short_summary_end_idx = possible_values_index if possible_values_index >= 0 else len(short_summary)
short_summary = short_summary[0:short_summary_end_idx]
short_summary = short_summary.strip()
yield '{}:summary: {}'.format(self._DOUBLE_INDENT, short_summary)
yield '{}:description: {}'.format(self._DOUBLE_INDENT, arg.long_summary)
if arg.choices:
yield '{}:values: {}'.format(self._DOUBLE_INDENT,
', '.join(sorted([str(x) for x in arg.choices])))
if arg.default and arg.default != argparse.SUPPRESS:
try:
if arg.default.startswith(_USER_HOME):
arg.default = arg.default.replace(_USER_HOME, '~').replace('\\', '/')
except Exception: # pylint: disable=broad-except
pass
try:
arg.default = arg.default.replace("\\", "\\\\")
except Exception: # pylint: disable=broad-except
pass
yield '{}:default: {}'.format(self._DOUBLE_INDENT, arg.default)
if arg.value_sources:
yield '{}:source: {}'.format(self._DOUBLE_INDENT, ', '.join(self._get_param_value_sources(arg)))
yield ''
yield ''
if help_file.examples:
for e in help_file.examples:
yield '{}.. cliexample:: {}'.format(self._INDENT, e.short_summary)
yield ''
yield self._DOUBLE_INDENT + e.command.replace("\\", "\\\\")
yield ''
def run(self):
node = nodes.section()
node.document = self.state.document
result = ViewList()
for line in self.make_rst():
result.append(line, '<azhelpgen>')
nested_parse_with_titles(self.state, result, node)
return node.children
def _get_help_files(self, az_cli):
raise NotImplementedError()
def _load_doc_source_map(self):
raise NotImplementedError()
def _get_doc_source_content(self, doc_source_map, help_file):
raise NotImplementedError()
@staticmethod
def _get_param_value_sources(param):
commands = []
for value_source in param.value_sources:
try:
commands.append(value_source["link"]["command"])
except KeyError:
continue
return commands
def setup_common_directives(app):
""" Add common directives to the sphinx app. This should be called by setup(app) which sphinx searches for when
generating documentation from sphinx extensions.
:param app: The sphinx app
:return:
"""
_add_directive(app, 'cligroup', CliGroupDirective)
_add_directive(app, 'clicommand', CliCommandDirective)
_add_directive(app, 'cliarg', CliArgumentDirective)
_add_directive(app, 'cliexample', CliExampleDirective)
def _add_directive(app, name, cls):
# check based on similar check in Sphinx().add_directive
if name not in directives._directives: # pylint: disable=protected-access
app.add_directive(name, cls)