def testCommandChoice(self): tester = usage_text.TextChoiceSuggester(GCLOUD_COMMANDS) self.assertEqual('app', tester.GetSuggestion('apa')) self.assertEqual('config', tester.GetSuggestion('confg')) self.assertEqual('components', tester.GetSuggestion('componets')) self.assertEqual('app', tester.GetSuggestion('ap')) self.assertEqual('init', tester.GetSuggestion('int')) tester = usage_text.TextChoiceSuggester(['yaml', 'Ybad']) self.assertEqual('yaml', tester.GetSuggestion('YAML'))
def SetProperty(name, default_value, list_command): """Set named compute property to default_value or get via list command.""" if not default_value: values = self._RunCmd(list_command) if values is None: return values = list(values) message = ( 'Which Google Compute Engine {0} would you like to use as project ' 'default?\n' 'If you do not specify a {0} via a command line flag while working ' 'with Compute Engine resources, the default is assumed.').format( name) idx = console_io.PromptChoice( [value['name'] for value in values] + ['Do not set default {0}'.format(name)], message=message, prompt_string=None, allow_freeform=True, freeform_suggester=usage_text.TextChoiceSuggester()) if idx is None or idx == len(values): return default_value = values[idx] properties.PersistProperty(properties.VALUES.compute.Property(name), default_value['name']) log.status.write('Your project default Compute Engine {0} has been set ' 'to [{1}].\nYou can change it by running ' '[gcloud config set compute/{0} NAME].\n\n' .format(name, default_value['name'])) return default_value
def _PromptForProjectId(project_ids): """Prompt the user for a project ID, based on the list of available IDs. Also allows an option to create a project. Args: project_ids: list of str or None, the project IDs to prompt for. If this value is None, the listing was unsuccessful and we prompt the user free-form (and do not validate the input). If it's empty, we offer to create a project for the user. Returns: str, the project ID to use, or _CREATE_PROJECT_SENTINEL (if a project should be created), or None """ if project_ids is None: return console_io.PromptResponse( 'Enter project id you would like to use: ') or None elif not project_ids: if not console_io.PromptContinue( 'This account has no projects.', prompt_string='Would you like to create one?'): return None return _CREATE_PROJECT_SENTINEL else: idx = console_io.PromptChoice( project_ids + ['Create a new project'], message='Pick cloud project to use: ', allow_freeform=True, freeform_suggester=usage_text.TextChoiceSuggester()) if idx is None: return None elif idx == len(project_ids): return _CREATE_PROJECT_SENTINEL return project_ids[idx]
def testAliases(self): tester = usage_text.TextChoiceSuggester(GCLOUD_COMMANDS) tester.AddAliases(['foo', 'bar'], 'components') tester.AddAliases(['app'], 'components') self.assertEqual('components', tester.GetSuggestion('foo')) self.assertEqual('components', tester.GetSuggestion('fooo')) # Adding an alias for an existing item should not clobber it. self.assertEqual('app', tester.GetSuggestion('app'))
def _PromptForProjectId(project_ids, limit_exceeded): """Prompt the user for a project ID, based on the list of available IDs. Also allows an option to create a project. Args: project_ids: list of str or None, the project IDs to prompt for. If this value is None, the listing was unsuccessful and we prompt the user free-form (and do not validate the input). If it's empty, we offer to create a project for the user. limit_exceeded: bool, whether or not the project list limit was reached. If this limit is reached, then user will be prompted with a choice to manually enter a project id, create a new project, or list all projects. Returns: str, the project ID to use, or _CREATE_PROJECT_SENTINEL (if a project should be created), or None """ if project_ids is None: return console_io.PromptResponse( 'Enter project id you would like to use: ') or None elif not project_ids: if not console_io.PromptContinue( 'This account has no projects.', prompt_string='Would you like to create one?'): return None return _CREATE_PROJECT_SENTINEL elif limit_exceeded: idx = console_io.PromptChoice( ['Enter a project ID', 'Create a new project', 'List projects'], message=( 'This account has a lot of projects! Listing them all can ' 'take a while.')) if idx is None: return None elif idx == 0: return console_io.PromptWithValidator( _IsExistingProject, 'Project ID does not exist or is not active. Please enter an ' 'existing and active Project ID.', 'Enter an existing project id you would like to use: ') elif idx == 1: return _CREATE_PROJECT_SENTINEL else: project_ids = _GetProjectIds() idx = console_io.PromptChoice( project_ids + ['Create a new project'], message='Pick cloud project to use: ', allow_freeform=True, freeform_suggester=usage_text.TextChoiceSuggester()) if idx is None: return None elif idx == len(project_ids): return _CREATE_PROJECT_SENTINEL return project_ids[idx]
def _PickProject(self, preselected=None): """Allows user to select a project. Args: preselected: str, use this value if not None Returns: str, project_id or None if was not selected. """ try: projects = list(projects_api.List()) except Exception: # pylint: disable=broad-except log.debug('Failed to execute projects list: %s, %s, %s', *sys.exc_info()) projects = None if projects is None: # Failed to get the list. project_id = preselected or console_io.PromptResponse( 'Enter project id you would like to use: ') if not project_id: return None else: projects = sorted(projects, key=lambda prj: prj.projectId) choices = [project.projectId for project in projects] if not choices: log.status.write( '\nThis account has no projects. Please create one in ' 'developers console ' '(https://console.developers.google.com/project) ' 'before running this command.\n') return None if preselected: project_id = preselected project_names = [project.projectId for project in projects] if project_id not in project_names: log.status.write( '\n[{0}] is not one of your projects [{1}].\n'.format( project_id, ','.join(project_names))) return None elif len(choices) == 1: project_id = projects[0].projectId else: idx = console_io.PromptChoice( choices, message='Pick cloud project to use: ', allow_freeform=True, freeform_suggester=usage_text.TextChoiceSuggester()) if idx is None: return None project_id = projects[idx].projectId self._RunCmd(['config', 'set'], ['project', project_id]) log.status.write( 'Your current project has been set to: [{0}].\n\n'.format( project_id)) return project_id
def _Suggest(self, unknown_args): """Error out with a suggestion based on text distance for each unknown.""" messages = [] suggester = usage_text.TextChoiceSuggester() # pylint:disable=protected-access, This is an instance of this class. for flag in self._calliope_command.GetAllAvailableFlags(): options = flag.option_strings if options: # This is a flag, add all its names as choices. suggester.AddChoices(options) # Add any aliases as choices as well, but suggest the primary name. aliases = getattr(flag, 'suggestion_aliases', None) if aliases: suggester.AddAliases(aliases, options[0]) suggestions = {} for arg in unknown_args: # Only do this for flag names. if arg.startswith('--'): # Strip the flag value if any from the suggestion. flag = arg.split('=')[0] suggestion = suggester.GetSuggestion(flag) else: suggestion = None if suggestion: suggestions[arg] = suggestion messages.append(arg + " (did you mean '{0}'?)".format(suggestion)) else: messages.append(arg) # If there is a single arg, put it on the same line. If there are multiple # add each on it's own line for better clarity. separator = u'\n ' if len(messages) > 1 else u' ' # This try-except models the real parse_args() pathway to self.error(). try: raise parser_errors.UnrecognizedArgumentsError( u'unrecognized arguments:{0}{1}'.format( separator, separator.join(messages)), parser=self, total_unrecognized=len(unknown_args), total_suggestions=len(suggestions), suggestions=suggestions, ) except argparse.ArgumentError as e: self.error(e.message)
def testPromptSuggester(self): properties.VALUES.core.disable_prompts.Set(False) properties.VALUES.core.interactive_ux_style.Set( properties.VALUES.core.InteractiveUXStyles.NORMAL) self.WriteInput('hez', 'yoy', 'ovre', 'their') options = ['hey', 'you', 'over', 'there'] suggester = usage_text.TextChoiceSuggester(choices=options) result = console_io.PromptChoice(options, allow_freeform=True, freeform_suggester=suggester) self.AssertErrContains('[hez] not in list. Did you mean [hey]?') self.AssertErrContains('[yoy] not in list. Did you mean [you]?') self.AssertErrContains('[ovre] not in list. Did you mean [over]?') self.AssertErrContains('[their] not in list. Did you mean [there]?') self.assertEqual(result, None)
def _check_value(self, action, value): """Overrides argparse.ArgumentParser's ._check_value(action, value) method. Args: action: argparse.Action, The action being checked against this value. value: The command line argument provided that needs to correspond to this action. Raises: argparse.ArgumentError: If the action and value don't work together. """ is_subparser = isinstance(action, CloudSDKSubParsersAction) # When using tab completion, argcomplete monkey patches various parts of # argparse and interferes with the normal argument parsing flow. Here, we # need to set self._orig_class because argcomplete compares this # directly to argparse._SubParsersAction to see if it should recursively # patch this parser. It should really check to see if it is a subclass # but alas, it does not. If we don't set this, argcomplete will not patch, # our subparser and completions below this point wont work. Normally we # would just set this in action.IsValidChoice() but sometimes this # sub-element has already been loaded and is already in action.choices. In # either case, we still need argcomplete to patch this subparser so it # can compute completions below this point. if is_subparser and '_ARGCOMPLETE' in os.environ: # pylint:disable=protected-access, Required by argcomplete. action._orig_class = argparse._SubParsersAction # This is copied from this method in argparse's version of this method. if action.choices is None or value in action.choices: return # We add this to check if we can lazy load the element. if is_subparser and action.IsValidChoice(value): return # Not something we know, raise an error. # pylint:disable=protected-access cli_generator = self._calliope_command._cli_generator missing_components = cli_generator.ComponentsForMissingCommand( self._calliope_command.GetPath() + [value]) if missing_components: msg = ('You do not currently have this command group installed. Using ' 'it requires the installation of components: ' '[{missing_components}]'.format( missing_components=', '.join(missing_components))) update_manager.UpdateManager.EnsureInstalledAndRestart( missing_components, msg=msg) if is_subparser: # We are going to show the usage anyway, which requires loading # everything. Do this here so that choices gets populated. action.LoadAllChoices() # Command is not valid, see what we can suggest as a fix... message = u"Invalid choice: '{0}'.".format(value) # Determine if the requested command is available in another release track. existing_alternatives = self._ExistingAlternativeReleaseTracks(value) if existing_alternatives: message += (u'\nThis command is available in one or more alternate ' u'release tracks. Try:\n ') message += u'\n '.join(existing_alternatives) # Log to analytics the attempt to execute a command. # We know the user entered 'value' is a valid command in a different # release track. It's safe to include it. raise parser_errors.WrongTrackError( message, extra_path_arg=value, suggestions=existing_alternatives) # See if the spelling was close to something else that exists here. choices = sorted(action.choices) suggester = usage_text.TextChoiceSuggester(choices) suggester.AddSynonyms() if is_subparser: # Add command suggestions if the group registered any. cmd_suggestions = self._calliope_command._common_type.CommandSuggestions() cli_name = self._calliope_command.GetPath()[0] for cmd, suggestion in cmd_suggestions.iteritems(): suggester.AddAliases([cmd], cli_name + ' ' + suggestion) suggestion = suggester.GetSuggestion(value) if suggestion: message += " Did you mean '{0}'?".format(suggestion) elif not is_subparser: # Command group choices will be displayed in the usage message. message += '\n\nValid choices are [{0}].'.format(', '.join(choices)) # Log to analytics the attempt to execute a command. # We don't know if the user entered 'value' is a mistyped command or # some resource name that the user entered and we incorrectly thought it's # a command. We can't include it since it might be PII. raise parser_errors.UnknownCommandError( message, argument=action.option_strings[0] if action.option_strings else None, total_unrecognized=1, total_suggestions=1 if suggestion else 0, suggestions=[suggestion] if suggestion else choices, )
def parse_args(self, args=None, namespace=None): """Overrides argparse.ArgumentParser's .parse_args method.""" namespace, unknown_args = self.parse_known_args(args, namespace) if not unknown_args: return namespace # Content of these lines differs from argparser's parse_args(). # pylint:disable=protected-access deepest_parser = namespace._deepest_parser or self deepest_parser._specified_args = namespace._specified_args if deepest_parser._remainder_action: # Assume the user wanted to pass all arguments after last recognized # arguments into _remainder_action. Either do this with a warning or # fail depending on strictness. # pylint:disable=protected-access try: namespace, unknown_args = ( deepest_parser._remainder_action.ParseRemainingArgs( unknown_args, namespace, args)) # There still may be unknown_args that came before the last known arg. if not unknown_args: return namespace except parser_errors.UnrecognizedArgumentsError as e: # In the case of UnrecognizedArgumentsError, we want to just let it # continue so that we can get the nicer error handling. pass # There is at least one parsing error. Add a message for each unknown # argument. For each, try to come up with a suggestion based on text # distance. If one is close enough, print a 'did you mean' message along # with that argument. messages = [] suggester = usage_text.TextChoiceSuggester() # pylint:disable=protected-access, This is an instance of this class. for flag in deepest_parser._calliope_command.GetAllAvailableFlags(): options = flag.option_strings if options: # This is a flag, add all its names as choices. suggester.AddChoices(options) # Add any aliases as choices as well, but suggest the primary name. aliases = getattr(flag, 'suggestion_aliases', None) if aliases: suggester.AddAliases(aliases, options[0]) suggestions = {} for arg in unknown_args: # Only do this for flag names. if arg.startswith('--'): # Strip the flag value if any from the suggestion. flag = arg.split('=')[0] suggestion = suggester.GetSuggestion(flag) else: suggestion = None if suggestion: suggestions[arg] = suggestion messages.append(arg + " (did you mean '{0}'?)".format(suggestion)) else: messages.append(arg) # If there is a single arg, put it on the same line. If there are multiple # add each on it's own line for better clarity. separator = u'\n ' if len(messages) > 1 else u' ' # This try-except models the real parse_args() pathway to self.error(). try: raise parser_errors.UnrecognizedArgumentsError( u'unrecognized arguments:{0}{1}'.format( separator, separator.join(messages)), parser=deepest_parser, total_unrecognized=len(unknown_args), total_suggestions=len(suggestions), suggestions=suggestions, ) except argparse.ArgumentError as e: deepest_parser.error(e.message)
def testCommandChoice_DistanceTooFar(self): tester = usage_text.TextChoiceSuggester(['ssh']) self.assertEqual(None, tester.GetSuggestion('help'))