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