Example #1
0
    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
Example #2
0
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)