# -*- coding: utf-8 -*- # # Copyright 2015 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A collection of CLI walkers.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import io import os from googlecloudsdk.calliope import actions from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import cli_tree from googlecloudsdk.calliope import markdown from googlecloudsdk.calliope import walker from googlecloudsdk.core import properties from googlecloudsdk.core.document_renderers import render_document from googlecloudsdk.core.util import files from googlecloudsdk.core.util import pkg_resources import six _HELP_HTML_DATA_FILES = [ 'favicon.ico', 'index.html', '_menu_.css', '_menu_.js', '_title_.html', ] class DevSiteGenerator(walker.Walker): """Generates DevSite reference HTML in a directory hierarchy. This implements gcloud meta generate-help-docs --manpage-dir=DIRECTORY. Attributes: _directory: The DevSite reference output directory. _need_section_tag[]: _need_section_tag[i] is True if there are section subitems at depth i. This prevents the creation of empty 'section:' tags in the '_toc' files. _toc_root: The root TOC output stream. _toc_main: The current main (just under root) TOC output stream. """ _REFERENCE = '/sdk/gcloud/reference' # TOC reference directory offset. _TOC = '_toc.yaml' def __init__( self, cli, directory, hidden=False, progress_callback=None, restrict=None ): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The devsite output directory path name. hidden: Boolean indicating whether to consider the hidden CLI. progress_callback: f(float), The function to call to update the progress bar or None for no progress bar. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ super(DevSiteGenerator, self).__init__(cli, restrict=restrict) self._directory = directory files.MakeDir(self._directory) self._need_section_tag = [] toc_path = os.path.join(self._directory, self._TOC) self._toc_root = files.FileWriter(toc_path) self._toc_root.write('toc:\n') self._toc_root.write('- title: "gcloud Reference"\n') self._toc_root.write(' path: %s\n' % self._REFERENCE) self._toc_root.write(' section:\n') self._toc_main = None def Visit(self, node, parent, is_group): """Updates the TOC and Renders a DevSite doc for each node in the CLI tree. Args: node: group/command CommandCommon info. parent: The parent Visit() return value, None at the top level. is_group: True if node is a group, otherwise its is a command. Returns: The parent value, ignored here. """ def _UpdateTOC(): """Updates the DevSIte TOC.""" depth = len(command) - 1 if not depth: return title = ' '.join(command) while depth >= len(self._need_section_tag): self._need_section_tag.append(False) if depth == 1: if is_group: if self._toc_main: # Close the current main group toc if needed. self._toc_main.close() # Create a new main group toc. toc_path = os.path.join(directory, self._TOC) toc = files.FileWriter(toc_path) self._toc_main = toc toc.write('toc:\n') toc.write('- title: "%s"\n' % title) toc.write(' path: %s\n' % '/'.join([self._REFERENCE] + command[1:])) self._need_section_tag[depth] = True toc = self._toc_root indent = ' ' if is_group: toc.write( '%s- include: %s\n' % ( indent, '/'.join([self._REFERENCE] + command[1:] + [self._TOC]), ) ) return else: toc = self._toc_main indent = ' ' * (depth - 1) if self._need_section_tag[depth - 1]: self._need_section_tag[depth - 1] = False toc.write('%ssection:\n' % indent) title = command[-1] toc.write('%s- title: "%s"\n' % (indent, title)) toc.write( '%s path: %s\n' % (indent, '/'.join([self._REFERENCE] + command[1:])) ) self._need_section_tag[depth] = is_group # Set up the destination dir for this level. command = node.GetPath() if is_group: directory = os.path.join(self._directory, *command[1:]) files.MakeDir(directory, mode=0o755) else: directory = os.path.join(self._directory, *command[1:-1]) # Render the DevSite document. path = ( os.path.join(directory, 'index' if is_group else command[-1]) + '.html' ) # Currently, devsite pages from GDU are automatically mirrored to all other # universes. To display Universe Disclaimer Information section correctly on # all universes after mirroring, temporarily override universe_domain # property to force the info section generated in devsite pages. universe_domain = None if properties.VALUES.core.universe_domain.IsExplicitlySet(): universe_domain = properties.VALUES.core.universe_domain.Get() properties.VALUES.core.universe_domain.Set('universe') with files.FileWriter(path) as f: md = markdown.Markdown(node) render_document.RenderDocument( style='devsite', title=' '.join(command), fin=io.StringIO(md), out=f, command_node=node, ) # reset universe_domain properties.VALUES.core.universe_domain.Set(universe_domain) _UpdateTOC() return parent def Done(self): """Closes the TOC files after the CLI tree walk is done.""" self._toc_root.close() if self._toc_main: self._toc_main.close() class HelpTextGenerator(walker.Walker): """Generates help text files in a directory hierarchy. Attributes: _directory: The help text output directory. """ def __init__( self, cli, directory, hidden=False, progress_callback=None, restrict=None ): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The Help Text output directory path name. hidden: Boolean indicating whether to consider the hidden CLI. progress_callback: f(float), The function to call to update the progress bar or None for no progress bar. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ super(HelpTextGenerator, self).__init__( cli, progress_callback=progress_callback, restrict=restrict ) self._directory = directory files.MakeDir(self._directory) def Visit(self, node, parent, is_group): """Renders a help text doc for each node in the CLI tree. Args: node: group/command CommandCommon info. parent: The parent Visit() return value, None at the top level. is_group: True if node is a group, otherwise its is a command. Returns: The parent value, ignored here. """ # Set up the destination dir for this level. command = node.GetPath() if is_group: directory = os.path.join(self._directory, *command[1:]) else: directory = os.path.join(self._directory, *command[1:-1]) files.MakeDir(directory, mode=0o755) # Render the help text document. path = os.path.join(directory, 'GROUP' if is_group else command[-1]) with files.FileWriter(path) as f: md = markdown.Markdown(node) render_document.RenderDocument(style='text', fin=io.StringIO(md), out=f) return parent class DocumentGenerator(walker.Walker): """Generates style manpage files with suffix in an output directory. All files will be generated in one directory. Attributes: _directory: The document output directory. _style: The document style. _suffix: The output file suffix. """ def __init__(self, cli, directory, style, suffix, restrict=None): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The manpage output directory path name. style: The document style. suffix: The generate document file suffix. None for .
. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ super(DocumentGenerator, self).__init__(cli, restrict=restrict) self._directory = directory self._style = style self._suffix = suffix files.MakeDir(self._directory) def Visit(self, node, parent, is_group): """Renders document file for each node in the CLI tree. Args: node: group/command CommandCommon info. parent: The parent Visit() return value, None at the top level. is_group: True if node is a group, otherwise its is a command. Returns: The parent value, ignored here. """ if self._style == 'linter': meta_data = actions.GetCommandMetaData(node) else: meta_data = None command = node.GetPath() path = os.path.join(self._directory, '_'.join(command)) + self._suffix with files.FileWriter(path) as f: md = markdown.Markdown(node) render_document.RenderDocument( style=self._style, title=' '.join(command), fin=io.StringIO(md), out=f, command_metadata=meta_data, ) return parent class HtmlGenerator(DocumentGenerator): """Generates HTML manpage files with suffix .html in an output directory. The output directory will contain a man1 subdirectory containing all of the HTML manpage files. """ def WriteHtmlMenu(self, command, out): """Writes the command menu tree HTML on out. Args: command: dict, The tree (nested dict) of command/group names. out: stream, The output stream. """ def ConvertPathToIdentifier(path): return '_'.join(path) def WalkCommandTree(command, prefix): """Visit each command and group in the CLI command tree. Args: command: dict, The tree (nested dict) of command/group names. prefix: [str], The subcommand arg prefix. """ level = len(prefix) visibility = 'visible' if level <= 1 else 'hidden' indent = level * 2 + 2 name = command.get('_name_') args = prefix + [name] out.write( '{indent}
  • {name}'.format( indent=' ' * indent, visibility=visibility, name=name, item=ConvertPathToIdentifier(args), ) ) commands = command.get('commands', []) + command.get('groups', []) if commands: out.write('\n'.format(indent=' ' * (indent + 1))) out.write('{indent}
  • \n'.format(indent=' ' * indent)) else: out.write('\n') out.write("""\ man page tree navigation """) def _GenerateHtmlNav(self, directory, cli, hidden, restrict): """Generates html nav files in directory.""" tree = CommandTreeGenerator(cli).Walk(hidden, restrict) with files.FileWriter(os.path.join(directory, '_menu_.html')) as out: self.WriteHtmlMenu(tree, out) for file_name in _HELP_HTML_DATA_FILES: file_contents = pkg_resources.GetResource( 'googlecloudsdk.api_lib.meta.help_html_data.', file_name ) files.WriteBinaryFileContents( os.path.join(directory, file_name), file_contents ) def __init__( self, cli, directory, hidden=False, progress_callback=None, restrict=None ): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The HTML output directory path name. hidden: Boolean indicating whether to consider the hidden CLI. progress_callback: f(float), The function to call to update the progress bar or None for no progress bar. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ super(HtmlGenerator, self).__init__( cli, directory=directory, style='html', suffix='.html', restrict=restrict, ) self._GenerateHtmlNav(directory, cli, hidden, restrict) class ManPageGenerator(DocumentGenerator): """Generates manpage files with suffix .1 in an output directory. The output directory will contain a man1 subdirectory containing all of the manpage files. """ _SECTION_FORMAT = 'man{section}' def __init__( self, cli, directory, hidden=False, progress_callback=None, restrict=None ): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The manpage output directory path name. hidden: Boolean indicating whether to consider the hidden CLI. progress_callback: f(float), The function to call to update the progress bar or None for no progress bar. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ # Currently all gcloud manpages are in section 1. section_subdir = self._SECTION_FORMAT.format(section=1) section_dir = os.path.join(directory, section_subdir) super(ManPageGenerator, self).__init__( cli, directory=section_dir, style='man', suffix='.1', restrict=restrict ) class LinterGenerator(DocumentGenerator): """Generates linter files with suffix .json in an output directory.""" def __init__( self, cli, directory, hidden=False, progress_callback=None, restrict=None ): """Constructor. Args: cli: The Cloud SDK CLI object. directory: The linter output directory path name. hidden: Boolean indicating whether to consider the hidden CLI. progress_callback: f(float), The function to call to update the progress bar or None for no progress bar. restrict: Restricts the walk to the command/group dotted paths in this list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic'] restricts the walk to the 'gcloud topic' and 'gcloud alpha test' commands/groups. """ super(LinterGenerator, self).__init__( cli, directory=directory, style='linter', suffix='.json', restrict=restrict, ) class CommandTreeGenerator(walker.Walker): """Constructs a CLI command dict tree. This implements the resource generator for gcloud meta list-commands. Attributes: _with_flags: Include the non-global flags for each command/group if True. _with_flag_values: Include flag value choices or :type: if True. _global_flags: The set of global flags, only listed for the root command. """ def __init__(self, cli, with_flags=False, with_flag_values=False, **kwargs): """Constructor. Args: cli: The Cloud SDK CLI object. with_flags: Include the non-global flags for each command/group if True. with_flag_values: Include flags and flag value choices or :type: if True. **kwargs: Other keyword arguments to pass to Walker constructor. """ super(CommandTreeGenerator, self).__init__(cli, **kwargs) self._with_flags = with_flags or with_flag_values self._with_flag_values = with_flag_values self._global_flags = set() def Visit(self, node, parent, is_group): """Visits each node in the CLI command tree to construct the dict tree. Args: node: group/command CommandCommon info. parent: The parent Visit() return value, None at the top level. is_group: True if node is a group, otherwise its is a command. Returns: The subtree parent value, used here to construct a dict tree. """ name = node.name.replace('_', '-') info = {'_name_': name} if self._with_flags: all_flags = [] for arg in node.GetAllAvailableFlags(): value = None if self._with_flag_values: if arg.choices: choices = sorted(arg.choices) if choices != ['false', 'true']: value = ','.join([six.text_type(choice) for choice in choices]) elif isinstance(arg.type, int): value = ':int:' elif isinstance(arg.type, float): value = ':float:' elif isinstance(arg.type, arg_parsers.ArgDict): value = ':dict:' elif isinstance(arg.type, arg_parsers.ArgList): value = ':list:' elif arg.nargs != 0: metavar = arg.metavar or arg.dest.upper() value = ':' + metavar + ':' for f in arg.option_strings: if value: f += '=' + value all_flags.append(f) no_prefix = '--no-' flags = [] for flag in all_flags: if flag in self._global_flags: continue if flag.startswith(no_prefix): positive = '--' + flag[len(no_prefix) :] if positive in all_flags: continue flags.append(flag) if flags: info['_flags_'] = sorted(flags) if not self._global_flags: # Most command flags are global (defined by the root command) or # command-specific. Group-specific flags are rare. Separating out # the global flags streamlines command descriptions and prevents # global flag changes (we already have too many!) from making it # look like every command has changed. self._global_flags.update(flags) if is_group: if parent: if cli_tree.LOOKUP_GROUPS not in parent: parent[cli_tree.LOOKUP_GROUPS] = [] parent[cli_tree.LOOKUP_GROUPS].append(info) return info if cli_tree.LOOKUP_COMMANDS not in parent: parent[cli_tree.LOOKUP_COMMANDS] = [] parent[cli_tree.LOOKUP_COMMANDS].append(info) return None class GCloudTreeGenerator(walker.Walker): """Generates an external representation of the gcloud CLI tree. This implements the resource generator for gcloud meta list-gcloud. """ def Visit(self, node, parent, is_group): """Visits each node in the CLI command tree to construct the external rep. Args: node: group/command CommandCommon info. parent: The parent Visit() return value, None at the top level. is_group: True if node is a group, otherwise its is a command. Returns: The subtree parent value, used here to construct an external rep node. """ return cli_tree.Command(node, parent)