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 inspect_object(obj, accept_bound_methods=False): """ Used to inspect a function or method annotated with @command or @argument. Returns a well structured dict summarizing the metadata added through the decorators Check the module documentation for more info """ command = getattr(obj, "__command", None) arguments_decorator_specs = getattr(obj, "__arguments_decorator_specs", {}) argspec = get_arg_spec(obj) args = argspec.args # remove the first argument in case this is a method (normally the first # arg is 'self') if ismethod(obj): args = args[1:] result = {"arguments": OrderedDict(), "command": None, "subcommands": {}} if command: result["command"] = Command( name=command["name"] or obj.__name__, help=command["help"] or obj.__doc__, aliases=command["aliases"], exclusive_arguments=command["exclusive_arguments"], ) # Is this a super command? is_supercommand = isclass(obj) for i, arg in enumerate(args): if (is_supercommand or accept_bound_methods) and arg == "self": continue arg_idx_with_default = len(args) - len(argspec.defaults) default_value_set = bool(argspec.defaults and i >= arg_idx_with_default) default_value = ( argspec.defaults[i - arg_idx_with_default] if default_value_set else None ) # We will reject classes (super-commands) that has required arguments to # reduce complexity if is_supercommand and not default_value_set: raise ValueError( "Cannot accept super commands that has required " "arguments with no default value " "like '{}' in super-command '{}'".format( arg, result["command"].name ) ) arg_decor_spec = arguments_decorator_specs.get( arg, _empty_arg_decorator_spec(arg) ) result["arguments"][arg_decor_spec.name] = Argument( arg=arg_decor_spec.arg, description=arg_decor_spec.description, type=argspec.annotations.get(arg), default_value_set=default_value_set, default_value=default_value, name=arg_decor_spec.name, extra_names=arg_decor_spec.aliases, positional=arg_decor_spec.positional, choices=arg_decor_spec.choices, ) if argspec.varkw: # We will inject all the arguments that are not defined explicitly in # the function signature. for arg, arg_decor_spec in arguments_decorator_specs.items(): added_arguments = [v.name for v in result["arguments"].values()] if arg_decor_spec.name not in added_arguments: # This is an extra argument result["arguments"][arg_decor_spec.name] = Argument( arg=arg, description=arg_decor_spec.description, type=argspec.annotations.get(arg), default_value_set=True, default_value=None, name=arg_decor_spec.name, extra_names=arg_decor_spec.aliases, positional=arg_decor_spec.positional, choices=arg_decor_spec.choices, ) # Super Command Support if is_supercommand: result["subcommands"] = [] for attr in dir(obj): if attr.startswith("_"): # ignore "private" methods continue candidate = getattr(obj, attr) if not callable(candidate): # avoid e.g. properties continue metadata = inspect_object(candidate, accept_bound_methods=True) # ignore subcommands without docstring if not metadata.command.help: cprint((f"[WARNING] The sub-command {metadata.command.name} " "will not be loaded. " "Please provide a help message by either defining a " "docstring or filling the help argument in the " "@command annotation"), "red") continue if metadata.command: result["subcommands"].append((attr, metadata)) return FunctionInspection(**result)