def __init__(self, fn): self._built_in = False self._fn = fn if not callable(fn): raise ValueError("fn argument must be a callable") self._obj_metadata = inspect_object(fn) self._is_super_command = len(self.metadata.subcommands) > 0 self._subcommand_names = [] # We never expect a function to be passed here that has a self argument # In that case, we should get a bound method if "self" in self.metadata.arguments and not inspect.ismethod( self._fn): raise ValueError("Expecting either a function (eg. bar) or " "a bound method (eg. Foo().bar). " "You passed what appears to be an unbound method " "(eg. Foo.bar) it has a 'self' argument: %s" % function_to_str(fn)) if not self.metadata.command: raise ValueError("function or class {} needs to be annotated with " "@command".format(function_to_str(fn))) # If this is a super command, we need a completer for sub-commands if self.super_command: self._commands_completer = WordCompleter([], ignore_case=True, sentence=True) for _, inspection in self.metadata.subcommands: _sub_name = inspection.command.name self._commands_completer.words.append(_sub_name) self._commands_completer.meta_dict[_sub_name] = dedent( inspection.command.help).strip() self._subcommand_names.append(_sub_name)
def decorator(function): # Following makes interactive really slow. (T20898480) # This should be revisited in T20899641 # if (description is not None and \ # arg is not None and type is not None): # append_doc(function, arg, type, description) fn_specs = get_arg_spec(function) args = fn_specs.args or [] if arg not in args and not fn_specs.varkw: raise NameError( "Argument {} does not exist in function {}".format( arg, function_to_str(function) ) ) # init structures to store decorator data if not present _init_attr(function, "__annotations__", OrderedDict()) _init_attr(function, "__arguments_decorator_specs", {}) # Check if there is a conflict in type annotations current_type = function.__annotations__.get(arg) if current_type and type and current_type != type: raise TypeError( "Argument {} in {} is both specified as {} " "and {}".format( arg, function_to_str(function), current_type, type ) ) if arg in function.__arguments_decorator_specs: raise ValueError( "@argument decorator was applied twice " "for the same argument {} on function {}".format(arg, function) ) if positional and aliases: msg = "Aliases are not yet supported for positional arguments @ {}".format( arg ) raise ValueError(msg) # reject positional=True if we are applied over a class if isclass(function) and positional: raise ValueError( "Cannot set positional arguments for super " "commands" ) # We use __annotations__ to allow the usage of python 3 typing function.__annotations__.setdefault(arg, type) function.__arguments_decorator_specs[arg] = _ArgDecoratorSpec( arg=arg, description=description, name=name or transform_name(arg), aliases=aliases or [], positional=positional, choices=choices, ) return function
def _validate_exclusive_arguments(function, normalized_exclusive_arguments): if not normalized_exclusive_arguments: return exclusive_arguments = normalized_exclusive_arguments flat_ex_args = [arg for group in exclusive_arguments for arg in group] if not flat_ex_args: return inspection = inspect_object(function) possible_args = list(inspection.arguments.keys()) unknown_args = set(flat_ex_args) - set(possible_args) if unknown_args: msg = ( "The following arguments were specified as exclusive but they " "are not present in function {}: {}".format( function_to_str(function), ", ".join(unknown_args) ) ) raise NameError(msg) if len(set(flat_ex_args)) != len(flat_ex_args): counts = ( (item, group.count(item)) for group in exclusive_arguments for item in group ) repeated_args = [item for item, count in counts if count > 1] msg = ( "The following args are present in more than one exclusive " "group: {}".format(", ".join(repeated_args)) ) raise ValueError(msg)
def _get_arg_help(self, arg_meta): sb = ["["] if arg_meta.type: sb.append(function_to_str(arg_meta.type, False, False)) sb.append(", ") if arg_meta.default_value_set: sb.append("default: ") sb.append(arg_meta.default_value) else: sb.append("required") sb.append("] ") sb.append(arg_meta.description if arg_meta. description else "<no description provided>") return "".join(str(item) for item in sb)
def run_interactive(self, cmd, args, raw): try: args_metadata = self.metadata.arguments parsed = parser.parse(args, expect_subcommand=self.super_command) # prepare args dict parsed_dict = parsed.asDict() args_dict = parsed.kv.asDict() key_values = parsed.kv.asDict() command_name = cmd # if this is a super command, we need first to create an instance of # the class (fn) and pass the right arguments if self.super_command: subcommand = parsed_dict.get("__subcommand__") if not subcommand: cprint( "A sub-command must be supplied, valid values: " "{}".format(", ".join(self._get_subcommands())), "red", ) return 2 sub_inspection = self.subcommand_metadata(subcommand) if not sub_inspection: cprint( "Invalid sub-command '{}', valid values: " "{}".format(subcommand, ", ".join(self._get_subcommands())), "red", ) return 2 instance, remaining_args = self._create_subcommand_obj( args_dict) assert instance args_dict = remaining_args key_values = copy.copy(args_dict) args_metadata = sub_inspection.arguments attrname = self._find_subcommand_attr(subcommand) command_name = subcommand assert attrname is not None fn = getattr(instance, attrname) else: # not a super-command, use use the function instead fn = self._fn positionals = parsed_dict[ "positionals"] if parsed.positionals != "" else [] # We only allow positionals for arguments that have positional=True # ِ We filter out the OrderedDict this way to ensure we don't lose the # order of the arguments. We also filter out arguments that have # been passed by name already. The order of the positional arguments # follows the order of the function definition. can_be_positional = self._positional_arguments( args_metadata, args_dict.keys()) if len(positionals) > len(can_be_positional): if len(can_be_positional) == 0: err = "This command does not support positional arguments" else: # We have more positionals than we should err = ( "This command only supports ({}) positional arguments, " "namely arguments ({}). You have passed {} arguments ({})" " instead!").format( len(can_be_positional), ", ".join(can_be_positional.keys()), len(positionals), ", ".join(str(x) for x in positionals), ) cprint(err, "red") return 2 # constuct key_value dict from positional arguments. args_from_positionals = { key: value for value, key in zip(positionals, can_be_positional) } # update the total arguments dict with the positionals args_dict.update(args_from_positionals) # Run some validations on number of arguments provided # do we have keys that are supplied in both positionals and # key_value style? duplicate_keys = set(args_from_positionals.keys()).intersection( set(key_values.keys())) if duplicate_keys: cprint( "Arguments '{}' have been passed already, cannot have" " duplicate keys".format(list(duplicate_keys)), "red", ) return 2 # check for verbosity override in kwargs ctx = context.get_context() old_verbose = ctx.args.verbose if "verbose" in args_dict: ctx.set_verbose(args_dict["verbose"]) del args_dict["verbose"] del key_values["verbose"] # do we have keys that we know nothing about? extra_keys = set(args_dict.keys()) - set(args_metadata) if extra_keys: cprint( "Unknown argument(s) {} were" " passed".format(list(extra_keys)), "magenta", ) return 2 # is there any required keys that were not resolved from positionals # nor key_values? missing_keys = set(args_metadata) - set(args_dict.keys()) if missing_keys: required_missing = [] for key in missing_keys: if not args_metadata[key].default_value_set: required_missing.append(key) if required_missing: cprint( "Missing required argument(s) {} for command" " {}".format(required_missing, command_name), "yellow", ) return 3 # convert expected types for arguments for key, value in args_dict.items(): target_type = args_metadata[key].type if target_type is None: target_type = str try: new_value = apply_typing(value, target_type) except ValueError: fn_name = function_to_str(target_type, False, False) cprint( 'Cannot convert value "{}" to {} on argument {}'. format(value, fn_name, key), "yellow", ) return 4 else: args_dict[key] = new_value # Validate that arguments with `choices` are supplied with the # acceptable values. for arg, value in args_dict.items(): choices = args_metadata[arg].choices if choices: # Validate the choices in the case of values and list of # values. if is_list_type(args_metadata[arg].type): bad_inputs = [v for v in value if v not in choices] if bad_inputs: cprint( f"Argument '{arg}' got an unexpected " f"value(s) '{bad_inputs}'. Expected one " f"or more of {choices}.", "red", ) return 4 elif value not in choices: cprint( f"Argument '{arg}' got an unexpected value " f"'{value}'. Expected one of " f"{choices}.", "red", ) return 4 # arguments appear to be fine, time to run the function try: # convert argument names back to match the function signature args_dict = { args_metadata[k].arg: v for k, v in args_dict.items() } if inspect.iscoroutinefunction(fn): loop = asyncio.get_event_loop() ret = loop.run_until_complete(fn(**args_dict)) else: ret = fn(**args_dict) ctx.set_verbose(old_verbose) except Exception as e: cprint("Error running command: {}".format(str(e)), "red") cprint("-" * 60, "yellow") traceback.print_exc(file=sys.stderr) cprint("-" * 60, "yellow") return 1 return ret except CommandParseError as e: cprint("Error parsing command", "red") cprint(cmd + " " + args, "white", attrs=["bold"]) cprint((" " * (e.col + len(cmd))) + "^", "white", attrs=["bold"]) cprint(str(e), "yellow") return 1
def test(expected, with_module, with_args): self.assertEqual(function_to_str(foo, with_module, with_args), expected)