1# -*- coding: utf-8 -*- #
2# Copyright 2013 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"""Backend stuff for the calliope.cli module.
17
18Not to be used by mortals.
19
20"""
21
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import unicode_literals
25
26import argparse
27import collections
28import re
29import textwrap
30
31from googlecloudsdk.calliope import actions
32from googlecloudsdk.calliope import arg_parsers
33from googlecloudsdk.calliope import base
34from googlecloudsdk.calliope import command_loading
35from googlecloudsdk.calliope import display
36from googlecloudsdk.calliope import exceptions
37from googlecloudsdk.calliope import parser_arguments
38from googlecloudsdk.calliope import parser_errors
39from googlecloudsdk.calliope import parser_extensions
40from googlecloudsdk.calliope import usage_text
41from googlecloudsdk.calliope.concepts import handlers
42from googlecloudsdk.core import log
43from googlecloudsdk.core import metrics
44from googlecloudsdk.core.util import text
45import six
46
47
48class _Notes(object):
49  """Auto-generated NOTES section helper."""
50
51  def __init__(self, explicit_notes=None):
52    self._notes = []
53    if explicit_notes:
54      self._notes.append(explicit_notes.rstrip())
55      self._paragraph = True
56    else:
57      self._paragraph = False
58
59  def AddLine(self, line):
60    """Adds a note line with preceding separator if not empty."""
61    if not line:
62      if line is None:
63        return
64    elif self._paragraph:
65      self._paragraph = False
66      self._notes.append('')
67    self._notes.append(line.rstrip())
68
69  def GetContents(self):
70    """Returns the notes contents as a single string."""
71    return '\n'.join(self._notes) if self._notes else None
72
73
74class CommandCommon(object):
75  """A base class for CommandGroup and Command.
76
77  It is responsible for extracting arguments from the modules and does argument
78  validation, since this is always the same for groups and commands.
79  """
80
81  def __init__(self, common_type, path, release_track, cli_generator,
82               parser_group, allow_positional_args, parent_group):
83    """Create a new CommandCommon.
84
85    Args:
86      common_type: base._Common, The actual loaded user written command or
87        group class.
88      path: [str], A list of group names that got us down to this command group
89        with respect to the CLI itself.  This path should be used for things
90        like error reporting when a specific element in the tree needs to be
91        referenced.
92      release_track: base.ReleaseTrack, The release track (ga, beta, alpha,
93        preview) that this command group is in.  This will apply to all commands
94        under it.
95      cli_generator: cli.CLILoader, The builder used to generate this CLI.
96      parser_group: argparse.Parser, The parser that this command or group will
97        live in.
98      allow_positional_args: bool, True if this command can have positional
99        arguments.
100      parent_group: CommandGroup, The parent of this command or group. None if
101        at the root.
102    """
103    self.category = common_type.category
104    self._parent_group = parent_group
105
106    self.name = path[-1]
107    # For the purposes of argparse and the help, we should use dashes.
108    self.cli_name = self.name.replace('_', '-')
109    log.debug('Loaded Command Group: %s', path)
110    path[-1] = self.cli_name
111    self._path = path
112    self.dotted_name = '.'.join(path)
113    self._cli_generator = cli_generator
114
115    # pylint: disable=protected-access
116    self._common_type = common_type
117    self._common_type._cli_generator = cli_generator
118    self._common_type._release_track = release_track
119
120    self.is_group = any([t == base.Group for t in common_type.__mro__])
121
122    if parent_group:
123      # Propagate down the hidden attribute.
124      if parent_group.IsHidden():
125        self._common_type._is_hidden = True
126      # Propagate down the unicode supported attribute.
127      if parent_group.IsUnicodeSupported():
128        self._common_type._is_unicode_supported = True
129      # Propagate down notices from the deprecation decorator.
130      if parent_group.Notices():
131        for tag, msg in six.iteritems(parent_group.Notices()):
132          self._common_type.AddNotice(tag, msg, preserve_existing=True)
133
134    self.detailed_help = getattr(self._common_type, 'detailed_help', {})
135    self._ExtractHelpStrings(self._common_type.__doc__)
136
137    self._AssignParser(
138        parser_group=parser_group,
139        allow_positional_args=allow_positional_args)
140
141  def Notices(self):
142    """Gets the notices of this command or group."""
143    return self._common_type.Notices()
144
145  def ReleaseTrack(self):
146    """Gets the release track of this command or group."""
147    return self._common_type.ReleaseTrack()
148
149  def IsHidden(self):
150    """Gets the hidden status of this command or group."""
151    return self._common_type.IsHidden()
152
153  def IsUnicodeSupported(self):
154    """Gets the unicode supported status of this command or group."""
155    return self._common_type.IsUnicodeSupported()
156
157  def IsRoot(self):
158    """Returns True if this is the root element in the CLI tree."""
159    return not self._parent_group
160
161  def _TopCLIElement(self):
162    """Gets the top group of this CLI."""
163    if self.IsRoot():
164      return self
165    # pylint: disable=protected-access
166    return self._parent_group._TopCLIElement()
167
168  def _ExtractHelpStrings(self, docstring):
169    """Extracts short help, long help and man page index from a docstring.
170
171    Sets self.short_help, self.long_help and self.index_help and adds release
172    track tags if needed.
173
174    Args:
175      docstring: The docstring from which short and long help are to be taken
176    """
177    self.short_help, self.long_help = usage_text.ExtractHelpStrings(docstring)
178
179    if 'brief' in self.detailed_help:
180      self.short_help = re.sub(r'\s', ' ', self.detailed_help['brief']).strip()
181    if self.short_help and not self.short_help.endswith('.'):
182      self.short_help += '.'
183
184    # Append any notice messages to command description and long_help
185    if self.Notices():
186      all_notices = ('\n\n' +
187                     '\n\n'.join(sorted(self.Notices().values())) +
188                     '\n\n')
189      description = self.detailed_help.get('DESCRIPTION')
190      if description:
191        self.detailed_help = dict(self.detailed_help)  # make a shallow copy
192        self.detailed_help['DESCRIPTION'] = (all_notices +
193                                             textwrap.dedent(description))
194      if self.short_help == self.long_help:
195        self.long_help += all_notices
196      else:
197        self.long_help = self.short_help + all_notices + self.long_help
198
199    self.index_help = self.short_help
200    if len(self.index_help) > 1:
201      if self.index_help[0].isupper() and not self.index_help[1].isupper():
202        self.index_help = self.index_help[0].lower() + self.index_help[1:]
203      if self.index_help[-1] == '.':
204        self.index_help = self.index_help[:-1]
205
206    # Add an annotation to the help strings to mark the release stage.
207    # TODO(b/32361958): Clean Up ReleaseTracks to Leverage Notices().
208    tags = []
209    tag = self.ReleaseTrack().help_tag
210    if tag:
211      tags.append(tag)
212    if self.Notices():
213      tags.extend(sorted(self.Notices().keys()))
214    if tags:
215      tag = ' '.join(tags) + ' '
216
217      def _InsertTag(txt):
218        return re.sub(r'^(\s*)', r'\1' + tag, txt)
219
220      self.short_help = _InsertTag(self.short_help)
221      # If long_help starts with section markdown then it's not the implicit
222      # DESCRIPTION section and shouldn't have a tag inserted.
223      if not self.long_help.startswith('#'):
224        self.long_help = _InsertTag(self.long_help)
225
226      # No need to tag DESCRIPTION if it starts with {description} or {index}
227      # because they are already tagged.
228      description = self.detailed_help.get('DESCRIPTION')
229      if description and not re.match(r'^[ \n]*\{(description|index)\}',
230                                      description):
231        self.detailed_help = dict(self.detailed_help)  # make a shallow copy
232        self.detailed_help['DESCRIPTION'] = _InsertTag(
233            textwrap.dedent(description))
234
235  def GetNotesHelpSection(self, contents=None):
236    """Returns the NOTES section with explicit and generated help."""
237    if not contents:
238      contents = self.detailed_help.get('NOTES')
239    notes = _Notes(contents)
240    if self.IsHidden():
241      notes.AddLine('This command is an internal implementation detail and may '
242                    'change or disappear without notice.')
243    notes.AddLine(self.ReleaseTrack().help_note)
244    alternates = self.GetExistingAlternativeReleaseTracks()
245    if alternates:
246      notes.AddLine('{} also available:'.format(
247          text.Pluralize(
248              len(alternates), 'This variant is', 'These variants are')))
249      notes.AddLine('')
250      for alternate in alternates:
251        notes.AddLine('  $ ' + alternate)
252        notes.AddLine('')
253    return notes.GetContents()
254
255  def _AssignParser(self, parser_group, allow_positional_args):
256    """Assign a parser group to model this Command or CommandGroup.
257
258    Args:
259      parser_group: argparse._ArgumentGroup, the group that will model this
260          command or group's arguments.
261      allow_positional_args: bool, Whether to allow positional args for this
262          group or not.
263
264    """
265    if not parser_group:
266      # This is the root of the command tree, so we create the first parser.
267      self._parser = parser_extensions.ArgumentParser(
268          description=self.long_help,
269          add_help=False,
270          prog=self.dotted_name,
271          calliope_command=self)
272    else:
273      # This is a normal sub group, so just add a new subparser to the existing
274      # one.
275      self._parser = parser_group.add_parser(
276          self.cli_name,
277          help=self.short_help,
278          description=self.long_help,
279          add_help=False,
280          prog=self.dotted_name,
281          calliope_command=self)
282
283    self._sub_parser = None
284
285    self.ai = parser_arguments.ArgumentInterceptor(
286        parser=self._parser,
287        is_global=not parser_group,
288        cli_generator=self._cli_generator,
289        allow_positional=allow_positional_args)
290
291    self.ai.add_argument(
292        '-h', action=actions.ShortHelpAction(self),
293        is_replicated=True,
294        category=base.COMMONLY_USED_FLAGS,
295        help='Print a summary help and exit.')
296    self.ai.add_argument(
297        '--help', action=actions.RenderDocumentAction(self, '--help'),
298        is_replicated=True,
299        category=base.COMMONLY_USED_FLAGS,
300        help='Display detailed help.')
301    self.ai.add_argument(
302        '--document', action=actions.RenderDocumentAction(self),
303        is_replicated=True,
304        nargs=1,
305        metavar='ATTRIBUTES',
306        type=arg_parsers.ArgDict(),
307        hidden=True,
308        help='THIS TEXT SHOULD BE HIDDEN')
309
310    self._AcquireArgs()
311
312  def IsValidSubPath(self, command_path):
313    """Determines if the given sub command path is valid from this node.
314
315    Args:
316      command_path: [str], The pieces of the command path.
317
318    Returns:
319      True, if the given path parts exist under this command or group node.
320      False, if the sub path does not lead to a valid command or group.
321    """
322    current = self
323    for part in command_path:
324      current = current.LoadSubElement(part)
325      if not current:
326        return False
327    return True
328
329  def AllSubElements(self):
330    """Gets all the sub elements of this group.
331
332    Returns:
333      set(str), The names of all sub groups or commands under this group.
334    """
335    return []
336
337  # pylint: disable=unused-argument
338  def LoadAllSubElements(self, recursive=False, ignore_load_errors=False):
339    """Load all the sub groups and commands of this group.
340
341    Args:
342      recursive: bool, True to continue loading all sub groups, False, to just
343        load the elements under the group.
344      ignore_load_errors: bool, True to ignore command load failures. This
345        should only be used when it is not critical that all data is returned,
346        like for optimizations like static tab completion.
347
348    Returns:
349      int, The total number of elements loaded.
350    """
351    return 0
352
353  def LoadSubElement(self, name, allow_empty=False,
354                     release_track_override=None):
355    """Load a specific sub group or command.
356
357    Args:
358      name: str, The name of the element to load.
359      allow_empty: bool, True to allow creating this group as empty to start
360        with.
361      release_track_override: base.ReleaseTrack, Load the given sub-element
362        under the given track instead of that of the parent. This should only
363        be used when specifically creating the top level release track groups.
364
365    Returns:
366      _CommandCommon, The loaded sub element, or None if it did not exist.
367    """
368    pass
369
370  def LoadSubElementByPath(self, path):
371    """Load a specific sub group or command by path.
372
373    If path is empty, returns the current element.
374
375    Args:
376      path: list of str, The names of the elements to load down the hierarchy.
377
378    Returns:
379      _CommandCommon, The loaded sub element, or None if it did not exist.
380    """
381    curr = self
382    for part in path:
383      curr = curr.LoadSubElement(part)
384      if curr is None:
385        return None
386    return curr
387
388  def GetPath(self):
389    return self._path
390
391  def GetUsage(self):
392    return usage_text.GetUsage(self, self.ai)
393
394  def GetSubCommandHelps(self):
395    return {}
396
397  def GetSubGroupHelps(self):
398    return {}
399
400  def _AcquireArgs(self):
401    """Calls the functions to register the arguments for this module."""
402    # A Command subclass can define a _Flags() method.
403    self._common_type._Flags(self.ai)  # pylint: disable=protected-access
404    # A command implementation can optionally define an Args() method.
405    self._common_type.Args(self.ai)
406
407    if self._parent_group:
408      # Add parent arguments to the list of all arguments.
409      for arg in self._parent_group.ai.arguments:
410        self.ai.arguments.append(arg)
411      # Add parent concepts to children, if they aren't represented already
412      if self._parent_group.ai.concept_handler:
413        if not self.ai.concept_handler:
414          self.ai.add_concepts(handlers.RuntimeHandler())
415        # pylint: disable=protected-access
416        for concept_details in self._parent_group.ai.concept_handler._all_concepts:
417          try:
418            self.ai.concept_handler.AddConcept(**concept_details)
419          except handlers.RepeatedConceptName:
420            raise parser_errors.ArgumentException(
421                'repeated concept in {command}: {concept_name}'.format(
422                    command=self.dotted_name,
423                    concept_name=concept_details['name']))
424      # Add parent flags to children, if they aren't represented already
425      for flag in self._parent_group.GetAllAvailableFlags():
426        if flag.is_replicated:
427          # Each command or group gets its own unique help flags.
428          continue
429        if flag.do_not_propagate:
430          # Don't propagate down flags that only apply to the group but not to
431          # subcommands.
432          continue
433        if flag.is_required:
434          # It is not easy to replicate required flags to subgroups and
435          # subcommands, since then there would be two+ identical required
436          # flags, and we'd want only one of them to be necessary.
437          continue
438        try:
439          self.ai.AddFlagActionFromAncestors(flag)
440        except argparse.ArgumentError:
441          raise parser_errors.ArgumentException(
442              'repeated flag in {command}: {flag}'.format(
443                  command=self.dotted_name,
444                  flag=flag.option_strings))
445      # Update parent display_info in children, children take precedence.
446      self.ai.display_info.AddLowerDisplayInfo(
447          self._parent_group.ai.display_info)
448
449  def GetAllAvailableFlags(self, include_global=True, include_hidden=True):
450    flags = self.ai.flag_args + self.ai.ancestor_flag_args
451    # TODO(b/35983142): Use mutant disable decorator when its available.
452    # This if statement triggers a mutant. Currently there are no Python comment
453    # decorators to disable individual mutants. This statement is a semantic
454    # mutant space/time optimization (if the list in hand is OK then use it),
455    # and the mutant scanner can't detect those in a reasonable amount of time.
456    if include_global and include_hidden:
457      return flags
458    return [f for f in flags if
459            (include_global or not f.is_global) and
460            (include_hidden or not f.hidden)]
461
462  def GetSpecificFlags(self, include_hidden=True):
463    flags = self.ai.flag_args
464    if include_hidden:
465      return flags
466    return [f for f in flags if not f.hidden]
467
468  def GetExistingAlternativeReleaseTracks(self, value=None):
469    """Gets the names for the command in other release tracks.
470
471    Args:
472      value: str, Optional value being parsed after the command.
473
474    Returns:
475      [str]: The names for the command in other release tracks.
476    """
477    existing_alternatives = []
478    # Get possible alternatives.
479    path = self.GetPath()
480    if value:
481      path.append(value)
482    alternates = self._cli_generator.ReplicateCommandPathForAllOtherTracks(path)
483    # See if the command is actually enabled in any of those alternative tracks.
484    if alternates:
485      top_element = self._TopCLIElement()
486      # Pre-sort by the release track prefix so GA commands always list first.
487      for _, command_path in sorted(six.iteritems(alternates),
488                                    key=lambda x: x[0].prefix or ''):
489        alternative_cmd = top_element.LoadSubElementByPath(command_path[1:])
490        if alternative_cmd and not alternative_cmd.IsHidden():
491          existing_alternatives.append(' '.join(command_path))
492    return existing_alternatives
493
494
495class CommandGroup(CommandCommon):
496  """A class to encapsulate a group of commands."""
497
498  def __init__(self, impl_paths, path, release_track, construction_id,
499               cli_generator, parser_group, parent_group=None,
500               allow_empty=False):
501    """Create a new command group.
502
503    Args:
504      impl_paths: [str], A list of file paths to the command implementation for
505        this group.
506      path: [str], A list of group names that got us down to this command group
507        with respect to the CLI itself.  This path should be used for things
508        like error reporting when a specific element in the tree needs to be
509        referenced.
510      release_track: base.ReleaseTrack, The release track (ga, beta, alpha) that
511        this command group is in.  This will apply to all commands under it.
512      construction_id: str, A unique identifier for the CLILoader that is
513        being constructed.
514      cli_generator: cli.CLILoader, The builder used to generate this CLI.
515      parser_group: the current argparse parser, or None if this is the root
516        command group.  The root command group will allocate the initial
517        top level argparse parser.
518      parent_group: CommandGroup, The parent of this group. None if at the
519        root.
520      allow_empty: bool, True to allow creating this group as empty to start
521        with.
522
523    Raises:
524      LayoutException: if the module has no sub groups or commands
525    """
526    common_type = command_loading.LoadCommonType(
527        impl_paths, path, release_track, construction_id, is_command=False)
528    super(CommandGroup, self).__init__(
529        common_type,
530        path=path,
531        release_track=release_track,
532        cli_generator=cli_generator,
533        allow_positional_args=False,
534        parser_group=parser_group,
535        parent_group=parent_group)
536
537    self._construction_id = construction_id
538
539    # find sub groups and commands
540    self.groups = {}
541    self.commands = {}
542    self._groups_to_load = {}
543    self._commands_to_load = {}
544    self._unloadable_elements = set()
545
546    group_infos, command_infos = command_loading.FindSubElements(impl_paths,
547                                                                 path)
548    self._groups_to_load.update(group_infos)
549    self._commands_to_load.update(command_infos)
550
551    if (not allow_empty and
552        not self._groups_to_load and not self._commands_to_load):
553      raise command_loading.LayoutException(
554          'Group {0} has no subgroups or commands'.format(self.dotted_name))
555    # Initialize the sub-parser so sub groups can be found.
556    self.SubParser()
557
558  def CopyAllSubElementsTo(self, other_group, ignore):
559    """Copies all the sub groups and commands from this group to the other.
560
561    Args:
562      other_group: CommandGroup, The other group to populate.
563      ignore: set(str), Names of elements not to copy.
564    """
565    # pylint: disable=protected-access, This is the same class.
566    other_group._groups_to_load.update(
567        {name: impl_paths
568         for name, impl_paths in six.iteritems(self._groups_to_load)
569         if name not in ignore})
570    other_group._commands_to_load.update(
571        {name: impl_paths
572         for name, impl_paths in six.iteritems(self._commands_to_load)
573         if name not in ignore})
574
575  def SubParser(self):
576    """Gets or creates the argparse sub parser for this group.
577
578    Returns:
579      The argparse subparser that children of this group should register with.
580          If a sub parser has not been allocated, it is created now.
581    """
582    if not self._sub_parser:
583      # pylint: disable=protected-access
584      self._sub_parser = self._parser.add_subparsers(
585          action=parser_extensions.CommandGroupAction,
586          calliope_command=self)
587    return self._sub_parser
588
589  def AllSubElements(self):
590    """Gets all the sub elements of this group.
591
592    Returns:
593      set(str), The names of all sub groups or commands under this group.
594    """
595    return (set(self._groups_to_load.keys()) |
596            set(self._commands_to_load.keys()))
597
598  def IsValidSubElement(self, name):
599    """Determines if the given name is a valid sub group or command.
600
601    Args:
602      name: str, The name of the possible sub element.
603
604    Returns:
605      bool, True if the name is a valid sub element of this group.
606    """
607    return bool(self.LoadSubElement(name))
608
609  def LoadAllSubElements(self, recursive=False, ignore_load_errors=False):
610    """Load all the sub groups and commands of this group.
611
612    Args:
613      recursive: bool, True to continue loading all sub groups, False, to just
614        load the elements under the group.
615      ignore_load_errors: bool, True to ignore command load failures. This
616        should only be used when it is not critical that all data is returned,
617        like for optimizations like static tab completion.
618
619    Returns:
620      int, The total number of elements loaded.
621    """
622    total = 0
623    for name in self.AllSubElements():
624      try:
625        element = self.LoadSubElement(name)
626        total += 1
627      # pylint:disable=bare-except, We are in a mode where accuracy doesn't
628      # matter. Just ignore any errors in loading a command.
629      except:
630        element = None
631        if not ignore_load_errors:
632          raise
633      if element and recursive:
634        total += element.LoadAllSubElements(
635            recursive=recursive, ignore_load_errors=ignore_load_errors)
636    return total
637
638  def LoadSubElement(self, name, allow_empty=False,
639                     release_track_override=None):
640    """Load a specific sub group or command.
641
642    Args:
643      name: str, The name of the element to load.
644      allow_empty: bool, True to allow creating this group as empty to start
645        with.
646      release_track_override: base.ReleaseTrack, Load the given sub-element
647        under the given track instead of that of the parent. This should only
648        be used when specifically creating the top level release track groups.
649
650    Returns:
651      _CommandCommon, The loaded sub element, or None if it did not exist.
652    """
653    name = name.replace('-', '_')
654
655    # See if this element has already been loaded.
656    existing = self.groups.get(name, None)
657    if not existing:
658      existing = self.commands.get(name, None)
659    if existing:
660      return existing
661    if name in self._unloadable_elements:
662      return None
663
664    element = None
665    try:
666      if name in self._groups_to_load:
667        element = CommandGroup(
668            self._groups_to_load[name], self._path + [name],
669            release_track_override or self.ReleaseTrack(),
670            self._construction_id, self._cli_generator, self.SubParser(),
671            parent_group=self, allow_empty=allow_empty)
672        self.groups[element.name] = element
673      elif name in self._commands_to_load:
674        element = Command(
675            self._commands_to_load[name], self._path + [name],
676            release_track_override or self.ReleaseTrack(),
677            self._construction_id, self._cli_generator, self.SubParser(),
678            parent_group=self)
679        self.commands[element.name] = element
680    except command_loading.ReleaseTrackNotImplementedException as e:
681      self._unloadable_elements.add(name)
682      log.debug(e)
683    return element
684
685  def GetSubCommandHelps(self):
686    return dict(
687        (item.cli_name,
688         usage_text.HelpInfo(help_text=item.short_help,
689                             is_hidden=item.IsHidden(),
690                             release_track=item.ReleaseTrack))
691        for item in self.commands.values())
692
693  def GetSubGroupHelps(self):
694    return dict(
695        (item.cli_name,
696         usage_text.HelpInfo(help_text=item.short_help,
697                             is_hidden=item.IsHidden(),
698                             release_track=item.ReleaseTrack()))
699        for item in self.groups.values())
700
701  def RunGroupFilter(self, context, args):
702    """Constructs and runs the Filter() method of all parent groups.
703
704    This recurses up to the root group and then constructs each group and runs
705    its Filter() method down the tree.
706
707    Args:
708      context: {}, The context dictionary that Filter() can modify.
709      args: The argparse namespace.
710    """
711    if self._parent_group:
712      self._parent_group.RunGroupFilter(context, args)
713    self._common_type().Filter(context, args)
714
715  def GetCategoricalUsage(self):
716    return usage_text.GetCategoricalUsage(
717        self, self._GroupSubElementsByCategory())
718
719  def GetUncategorizedUsage(self):
720    return usage_text.GetUncategorizedUsage(self)
721
722  def GetHelpHint(self):
723    return usage_text.GetHelpHint(self)
724
725  def _GroupSubElementsByCategory(self):
726    """Returns dictionary mapping each category to its set of subelements."""
727
728    def _GroupSubElementsOfSameTypeByCategory(elements):
729      """Returns dictionary mapping specific to element type."""
730      categorized_dict = collections.defaultdict(set)
731      for element in elements.values():
732        if not element.IsHidden():
733          if element.category:
734            categorized_dict[element.category].add(element)
735          else:
736            categorized_dict[base.UNCATEGORIZED_CATEGORY].add(element)
737      return categorized_dict
738
739    self.LoadAllSubElements()
740    categories = {}
741    categories['command'] = (
742        _GroupSubElementsOfSameTypeByCategory(self.commands))
743    categories['command_group'] = (
744        _GroupSubElementsOfSameTypeByCategory(self.groups))
745
746    return categories
747
748
749class Command(CommandCommon):
750  """A class that encapsulates the configuration for a single command."""
751
752  def __init__(self, impl_paths, path, release_track, construction_id,
753               cli_generator, parser_group, parent_group=None):
754    """Create a new command.
755
756    Args:
757      impl_paths: [str], A list of file paths to the command implementation for
758        this command.
759      path: [str], A list of group names that got us down to this command
760        with respect to the CLI itself.  This path should be used for things
761        like error reporting when a specific element in the tree needs to be
762        referenced.
763      release_track: base.ReleaseTrack, The release track (ga, beta, alpha) that
764        this command group is in.  This will apply to all commands under it.
765      construction_id: str, A unique identifier for the CLILoader that is
766        being constructed.
767      cli_generator: cli.CLILoader, The builder used to generate this CLI.
768      parser_group: argparse.Parser, The parser to be used for this command.
769      parent_group: CommandGroup, The parent of this command.
770    """
771    common_type = command_loading.LoadCommonType(
772        impl_paths, path, release_track, construction_id, is_command=True,
773        yaml_command_translator=cli_generator.yaml_command_translator)
774    super(Command, self).__init__(
775        common_type,
776        path=path,
777        release_track=release_track,
778        cli_generator=cli_generator,
779        allow_positional_args=True,
780        parser_group=parser_group,
781        parent_group=parent_group)
782
783    self._parser.set_defaults(calliope_command=self, command_path=self._path)
784
785  def Run(self, cli, args):
786    """Run this command with the given arguments.
787
788    Args:
789      cli: The cli.CLI object for this command line tool.
790      args: The arguments for this command as a namespace.
791
792    Returns:
793      The object returned by the module's Run() function.
794
795    Raises:
796      exceptions.Error: if thrown by the Run() function.
797      exceptions.ExitCodeNoError: if the command is returning with a non-zero
798        exit code.
799    """
800    metrics.Loaded()
801
802    tool_context = {}
803    if self._parent_group:
804      self._parent_group.RunGroupFilter(tool_context, args)
805
806    command_instance = self._common_type(cli=cli, context=tool_context)
807
808    base.LogCommand(self.dotted_name, args)
809    resources = command_instance.Run(args)
810    resources = display.Displayer(command_instance, args, resources,
811                                  display_info=self.ai.display_info).Display()
812    metrics.Ran()
813
814    if command_instance.exit_code != 0:
815      raise exceptions.ExitCodeNoError(exit_code=command_instance.exit_code)
816
817    return resources
818