1# -*- coding: utf-8 -*- #
2# Copyright 2017 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""The cli_tree command help document markdown generator.
17
18This module generates command help markdown from the tree generated by:
19
20  gcloud --quiet alpha  # make sure the alpha component is installed
21  gcloud --quiet beta   # make sure the beta component is installed
22  gcloud meta list-gcloud --format=json |
23  python -c "
24    import json
25    import sys
26    data = json.load(sys.stdin)
27    print 'gcloud_tree =', data" > gcloud_tree.py
28
29Usage:
30
31  from googlecloudsdk.calliope import cli_tree_markdown
32  from googlecloudsdk.command_lib.shell import gcloud_tree
33
34  command = <command node in gcloud tree>
35  flag = <flag node in gcloud tree>
36  generator = cli_tree_markdown.CliTreeMarkdownGenerator(command, gcloud_tree)
37  generator.PrintSynopsisSection()
38  generator.PrintFlagDefinition(flag)
39    ...
40  markdown = generator.Edit()
41"""
42
43from __future__ import absolute_import
44from __future__ import division
45from __future__ import unicode_literals
46
47from googlecloudsdk.calliope import arg_parsers
48from googlecloudsdk.calliope import base
49from googlecloudsdk.calliope import cli_tree
50from googlecloudsdk.calliope import markdown
51from googlecloudsdk.calliope import usage_text
52from googlecloudsdk.core import properties
53
54import six
55
56
57if six.PY2:
58  FLAG_TYPE_NAME = b'flag'
59  POSITIONAL_TYPE_NAME = b'positional'
60  GROUP_TYPE_NAME = b'group'
61else:
62  FLAG_TYPE_NAME = 'flag'
63  POSITIONAL_TYPE_NAME = 'positional'
64  GROUP_TYPE_NAME = 'group'
65
66
67def _GetReleaseTrackFromId(release_id):
68  """Returns the base.ReleaseTrack for release_id."""
69  if release_id == 'INTERNAL':
70    release_id = 'GA'
71  return base.ReleaseTrack.FromId(release_id)
72
73
74def Flag(d):
75  """Returns a flag object suitable for the calliope.markdown module."""
76  flag = type(FLAG_TYPE_NAME, (object,), d)
77  flag.is_group = False
78  flag.is_hidden = d.get(cli_tree.LOOKUP_IS_HIDDEN, d.get('hidden', False))
79  flag.hidden = flag.is_hidden
80  flag.is_positional = False
81  flag.is_required = d.get(cli_tree.LOOKUP_IS_REQUIRED,
82                           d.get(cli_tree.LOOKUP_REQUIRED, False))
83  flag.required = flag.is_required
84  flag.help = flag.description
85  flag.dest = flag.name.lower().replace('-', '_')
86  flag.metavar = flag.value
87  flag.option_strings = [flag.name]
88  if not hasattr(flag, 'default'):
89    flag.default = None
90
91  if flag.type == 'bool':
92    flag.nargs = 0
93  elif flag.nargs not in ('?', '*', '+'):
94    flag.nargs = 1
95  if flag.type == 'dict':
96    flag.type = arg_parsers.ArgDict()
97  elif flag.type == 'list':
98    flag.type = arg_parsers.ArgList()
99  elif flag.type == 'string':
100    flag.type = None
101
102  if flag.attr.get(cli_tree.LOOKUP_INVERTED_SYNOPSIS):
103    flag.inverted_synopsis = True
104  prop = flag.attr.get('property')
105  if prop:
106    if cli_tree.LOOKUP_VALUE in prop:
107      kind = 'value'
108      value = prop[cli_tree.LOOKUP_VALUE]
109    else:
110      value = None
111      kind = 'bool' if flag.type == 'bool' else None
112    flag.store_property = (properties.FromString(
113        prop[cli_tree.LOOKUP_NAME]), kind, value)
114
115  return flag
116
117
118def Positional(d):
119  """Returns a positional object suitable for the calliope.markdown module."""
120  positional = type(POSITIONAL_TYPE_NAME, (object,), d)
121  positional.help = positional.description
122  positional.is_group = False
123  positional.is_hidden = False
124  positional.is_positional = True
125  positional.is_required = positional.nargs != '*'
126  positional.dest = positional.value.lower().replace('-', '_')
127  positional.metavar = positional.value
128  positional.option_strings = []
129  try:
130    positional.nargs = int(positional.nargs)
131  except ValueError:
132    pass
133  return positional
134
135
136def Argument(d):
137  """Returns an argument object suitable for the calliope.markdown module."""
138  if d.get(cli_tree.LOOKUP_IS_POSITIONAL, False):
139    return Positional(d)
140  if not d.get(cli_tree.LOOKUP_IS_GROUP, False):
141    return Flag(d)
142  group = type(GROUP_TYPE_NAME, (object,), d)
143  group.arguments = [Argument(a) for a in d.get(cli_tree.LOOKUP_ARGUMENTS, [])]
144  group.category = None
145  group.help = group.description
146  group.is_global = False
147  group.is_hidden = False
148  return group
149
150
151class CliTreeMarkdownGenerator(markdown.MarkdownGenerator):
152  """cli_tree command help markdown document generator.
153
154  Attributes:
155    _capsule: The help text capsule.
156    _command: The tree node for command.
157    _command_path: The command path list.
158    _tree: The (sub)tree root.
159    _sections: The help text sections indexed by SECTION name.
160    _subcommands: The dict of subcommand help indexed by subcommand name.
161    _subgroups: The dict of subgroup help indexed by subcommand name.
162  """
163
164  def __init__(self, command, tree):
165    """Constructor.
166
167    Args:
168      command: The command node in the root tree.
169      tree: The (sub)tree root.
170    """
171    self._tree = tree
172    self._command = command
173    self._command_path = command[cli_tree.LOOKUP_PATH]
174    super(CliTreeMarkdownGenerator, self).__init__(
175        self._command_path,
176        _GetReleaseTrackFromId(self._command[cli_tree.LOOKUP_RELEASE]),
177        self._command.get(cli_tree.LOOKUP_IS_HIDDEN,
178                          self._command.get('hidden', False)))
179    self._capsule = self._command[cli_tree.LOOKUP_CAPSULE]
180    self._sections = self._command[cli_tree.LOOKUP_SECTIONS]
181    self._subcommands = self.GetSubCommandHelp()
182    self._subgroups = self.GetSubGroupHelp()
183
184  def _GetCommandFromPath(self, command_path):
185    """Returns the command node for command_path."""
186    path = self._tree[cli_tree.LOOKUP_PATH]
187    if path:
188      # self._tree is not a super root. The first path name must match.
189      if command_path[:1] != path:
190        return None
191      # Already checked the first name.
192      command_path = command_path[1:]
193    command = self._tree
194    for name in command_path:
195      commands = command[cli_tree.LOOKUP_COMMANDS]
196      if name not in commands:
197        return None
198      command = commands[name]
199    return command
200
201  def IsValidSubPath(self, command_path):
202    """Returns True if the given command path after the top is valid."""
203    return self._GetCommandFromPath([cli_tree.DEFAULT_CLI_NAME] +
204                                    command_path) is not None
205
206  def GetArguments(self):
207    """Returns the command arguments."""
208    command = self._GetCommandFromPath(self._command_path)
209    try:
210      return [Argument(a) for a in
211              command[cli_tree.LOOKUP_CONSTRAINTS][cli_tree.LOOKUP_ARGUMENTS]]
212    except (KeyError, TypeError):
213      return []
214
215  def GetArgDetails(self, arg, depth=None):
216    """Returns the help text with auto-generated details for arg.
217
218    The help text was already generated on the cli_tree generation side.
219
220    Args:
221      arg: The arg to auto-generate help text for.
222      depth: The indentation depth at which the details should be printed.
223        Added here only to maintain consistency with superclass during testing.
224
225    Returns:
226      The help text with auto-generated details for arg.
227    """
228    return arg.help
229
230  def _GetSubHelp(self, is_group=False):
231    """Returns the help dict indexed by command for sub commands or groups."""
232    return {name: usage_text.HelpInfo(
233        help_text=subcommand[cli_tree.LOOKUP_CAPSULE],
234        is_hidden=subcommand.get(cli_tree.LOOKUP_IS_HIDDEN,
235                                 subcommand.get('hidden', False)),
236        release_track=_GetReleaseTrackFromId(
237            subcommand[cli_tree.LOOKUP_RELEASE]))
238            for name, subcommand in six.iteritems(self._command[
239                cli_tree.LOOKUP_COMMANDS])
240            if subcommand[cli_tree.LOOKUP_IS_GROUP] == is_group}
241
242  def GetSubCommandHelp(self):
243    """Returns the subcommand help dict indexed by subcommand."""
244    return self._GetSubHelp(is_group=False)
245
246  def GetSubGroupHelp(self):
247    """Returns the subgroup help dict indexed by subgroup."""
248    return self._GetSubHelp(is_group=True)
249
250  def PrintFlagDefinition(self, flag, disable_header=False):
251    """Prints a flags definition list item."""
252    if isinstance(flag, dict):
253      flag = Flag(flag)
254    super(CliTreeMarkdownGenerator, self).PrintFlagDefinition(
255        flag, disable_header=disable_header)
256
257  def _ExpandHelpText(self, doc):
258    """{...} references were done when the tree was generated."""
259    return doc
260
261
262def Markdown(command, tree):
263  """Returns the help markdown document string for the command node in tree.
264
265  Args:
266    command: The command node in the root tree.
267    tree: The (sub)tree root.
268
269  Returns:
270    The markdown document string.
271  """
272  return CliTreeMarkdownGenerator(command, tree).Generate()
273