Ejemplo n.º 1
0
    def __init__(self, options=None, commands=None, positionals=None):
        self.options = OrderedDict()
        self.add_option("__awwparse_help", HelpOption())
        self.add_options(self.__class__.options)
        if options is not None:
            self.add_options(options)

        self.commands = OrderedDict()
        self.add_commands(self.__class__.commands)
        if commands is not None:
            self.add_commands(commands)

        self.positionals = parse_positional_signature(
            force_list(self.__class__.positionals),
            require_metavar=True
        )
        if positionals is not None:
            self.add_positionals(positionals)

        self.parent = None

        signature = Signature.from_method(self.main)
        if signature.annotations:
            self._populate_from_signature(self, signature)

        for name in dir(self):
            try:
                attribute = getattr(self, name)
            except AttributeError:
                # maybe raised by properties; can be safely ignored
                continue
            if isinstance(attribute, Command):
                if not isinstance(attribute.main, MethodType):
                    attribute.main = partial(attribute.main, self)
                self.add_command(name, attribute)
Ejemplo n.º 2
0
 def test_move_to_end(self):
     d = OrderedDict([("foo", 1), ("bar", 2), ("baz", 3)])
     d.move_to_end("foo")
     self.assert_equal(d, OrderedDict([("bar", 2), ("baz", 3), ("foo", 1)]))
     d.move_to_end("foo", last=False)
     self.assert_equal(d, OrderedDict([("foo", 1), ("bar", 2), ("baz", 3)]))
Ejemplo n.º 3
0
 def test_popitem(self):
     d = OrderedDict([("foo", 1), ("bar", 2), ("baz", 3)])
     self.assert_equal(d.popitem(), ("baz", 3))
     self.assert_equal(d, OrderedDict([("foo", 1), ("bar", 2)]))
     self.assert_equal(d.popitem(last=False), ("foo", 1))
     self.assert_equal(d, OrderedDict([("bar", 2)]))
Ejemplo n.º 4
0
class Command(object):
    """
    Represents a command of a :class:`CLI` or another command.
    """
    #: A mapping of identifiers to options.
    options = []
    #: A mapping of command names to commands.
    commands = {}
    #: A positionals signature.
    positionals = ()
    #: A help message explaining this command.
    help = None

    @classmethod
    def _populate_from_signature(cls, command, signature):
        def lookup_annotation(name):
            try:
                return signature.annotations[name]
            except KeyError:
                raise ValueError("missing annotation for: {0}".format(name))
        for name in signature.positional_arguments:
            annotation = lookup_annotation(name)
            if isinstance(annotation, Option):
                command.add_option(name, annotation)
            elif isinstance(annotation, Positional):
                if name in signature.defaults:
                    command.add_option(
                        name,
                        Option("-" + name[0], "--" + name, annotation),
                        resolve_conflicts=True
                    )
                else:
                    annotation.metavar = name
                    command.add_positional(annotation)
            else:
                raise ValueError(
                    "unexpected annotation: {0!r}".format(annotation)
                )
        if signature.arbitary_positional_arguments is not None:
            name = signature.arbitary_positional_arguments
            annotation = lookup_annotation(name)
            if isinstance(annotation, Positional):
                annotation.metavar = name
                annotation.remaining = True
                command.add_positional(annotation)
            else:
                raise ValueError(
                    "unexpected annotation: {0!r}".format(annotation)
                )
        for name in signature.keyword_arguments:
            annotation = lookup_annotation(name)
            if isinstance(annotation, Positional):
                command.add_option(
                    name,
                    Option("-" + name[0], "--" + name[1:], annotation),
                    resolve_conflicts=True
                )
            elif isinstance(annotation, Option):
                command.add_option(name, annotation)
            else:
                raise ValueError(
                    "unexcepted annotation: {0!r}".format(annotation)
                )
        command.help = signature.documentation
        return command

    @classmethod
    def from_function(cls, *positionals):
        """
        Takes optional :class:`~Positional` objects corresponding to the
        arguments in the signature of the function passed to the returned
        function which returns a :class:`Command` object for the given
        annotated function. The :class:`~Positional` objects serve as
        alternative to annotations which are not available in Python 2.x.

        Positional arguments are turned into arguments, keyword arguments are
        turned into options and arbitary positional arguments are turned into
        an argument that takes all remaining ones.

        Each argument has to be given an annotation. Allowed annotations are
        :class:`~awwparse.positionals.Positional` objects. For keyword
        arguments you can also provide an :class:`Option` object.

        If an annotation is missing or has a wrong type a :exc:`ValueError` is
        raised.
        """
        def decorate(function):
            signature = Signature.from_function(function)
            if not signature.annotations:
                signature.annotations = dict(zip(signature.names, positionals))
            command = type(
                function.__name__,
                (cls, ),
                {
                    "__module__": function.__module__,
                    "__doc__": function.__doc__
                }
            )()
            command.main = function
            cls._populate_from_signature(command, signature)
            return command
        return decorate

    @classmethod
    def from_method(cls, *positionals):
        """
        Like :meth:`from_function` but for methods.

        Note that for instance and class methods you have to pass the class or
        instance with the `default_args` argument of :meth:`run` to the method.
        """
        def decorate(method):
            signature = Signature.from_method(method)
            if not signature.annotations:
                method.__annotations__ = dict(zip(signature.names, positionals))
            command = type(
                method.__name__,
                (cls, ),
                {
                    "__module__": method.__module__,
                    "__doc__": method.__doc__,
                    "main": staticmethod(method)
                }
            )()
            return command
        return decorate

    def __init__(self, options=None, commands=None, positionals=None):
        self.options = OrderedDict()
        self.add_option("__awwparse_help", HelpOption())
        self.add_options(self.__class__.options)
        if options is not None:
            self.add_options(options)

        self.commands = OrderedDict()
        self.add_commands(self.__class__.commands)
        if commands is not None:
            self.add_commands(commands)

        self.positionals = parse_positional_signature(
            force_list(self.__class__.positionals),
            require_metavar=True
        )
        if positionals is not None:
            self.add_positionals(positionals)

        self.parent = None

        signature = Signature.from_method(self.main)
        if signature.annotations:
            self._populate_from_signature(self, signature)

        for name in dir(self):
            try:
                attribute = getattr(self, name)
            except AttributeError:
                # maybe raised by properties; can be safely ignored
                continue
            if isinstance(attribute, Command):
                if not isinstance(attribute.main, MethodType):
                    attribute.main = partial(attribute.main, self)
                self.add_command(name, attribute)

    stdin = CLIAttribute("stdin")
    stdout = CLIAttribute("stdout")
    stderr = CLIAttribute("stderr")
    exit = CLIAttribute("exit")
    width = CLIAttribute("width")
    section_indent = CLIAttribute("section_indent")

    @property
    def option_prefixes(self):
        """
        A set of all option name prefixes.
        """
        return set(option.name_prefix for option in self.options)

    @property
    def abbreviated_option_prefixes(self):
        """
        A set of all abbreviated option name prefixes.
        """
        return set(
            option.abbreviation_prefix for option in self.options
        )

    @property
    def option_shorts(self):
        """
        A mapping of all abbreviated option argument names to options.
        """
        return dict(
            (option.short, option) for option in self.options
            if option.short is not None
        )

    @property
    def option_longs(self):
        """
        A mapping of all complete option argument names to options.
        """
        return dict(
            (option.long, option) for option in self.options
            if option.long is not None
        )

    def get_usage(self, arguments=None):
        result = [] if arguments is None else arguments.get_used(1)
        if self.options:
            result.extend(
                u("[{0}]").format(option.get_usage()) for option in self.options
            )
        if self.commands:
            result.append(u("{%s}") % u(",").join(self.commands))
        if self.positionals:
            result.extend(positional.usage for positional in self.positionals)
        return u(" ").join(result)

    def add_option(self, identifier, option, force=False,
                   resolve_conflicts=False):
        """
        Adds the `option` with `identifier` to the command.

        May raise an :exc:`OptionConflict` exception if the option name or it's
        abbreviation is  identical to those of another option. If `force` is
        ``True`` it will ignore the latter kind of conflict and replace the old
        option with the given one.  If `resolve_conflicts` is ``True``
        conflicts on argument names and abbreviations thereof will be resolved
        if possible by removing conflicting attributes.
        """
        conflicting_options = []
        if option.short in self.option_shorts:
            conflicting_options.append(
                (self.option_shorts[option.short], "short")
            )
        if option.long in self.option_longs:
            conflicting_options.append(
                (self.option_longs[option.long], "long")
            )
        option = option.copy()
        option.setdefault_metavars(identifier)
        for conflicting, reason in conflicting_options:
            if reason == "short":
                if resolve_conflicts and option.long is not None:
                    option.short = None
                    continue
                if force:
                    self.remove_option(conflicting)
                    continue
            elif reason == "long":
                if resolve_conflicts and option.short is not None:
                    option.long = None
                    continue
                if force:
                    self.remove_option(conflicting)
                    continue
            raise OptionConflict(
                u("given option {0!r} conflicts with the {1} of {2!r}").format(
                    option, reason, conflicting
                )
            )
        self.options[option] = identifier

    def add_options(self, options, force=False, resolve_conflicts=False):
        """
        Adds `options` from a given mapping.
        """
        for identifier, option in iter_mapping(options):
            self.add_option(
                identifier, option, force=force,
                resolve_conflicts=resolve_conflicts
            )

    def remove_option(self, to_be_removed_option):
        """
        Removes the given option.
        """
        del self.options[to_be_removed_option]

    def add_command(self, name, command, force=False):
        """
        Add the `command` with `name` to the command.

        May raise a :exc:`CommandConflict` if `name` is identical to that of
        another command unless `force` is ``True`` in which case the given
        `command` overwrites the confliciting one.
        """
        if not force and name in self.commands:
            raise CommandConflict(
                u("given command {0!r} conflicts with {1!r}").format(
                    command, self.commands[name]
                )
            )
        command.parent = self
        self.commands[name] = command

    def add_commands(self, commands, force=False):
        """
        Adds `commands` from a given mapping.
        """
        for name, command in iter_mapping(commands):
            self.add_command(name, command)

    def add_positional(self, positional):
        """
        Adds the given `positional` to the command.

        May raise an :exc:`PositionalConflict` if last positional takes all
        remaining command line arguments - in which case the added positional
        would never be reached.
        """
        if positional.metavar is None:
            raise ValueError("metavar not set on: {0!r}".format(positional))
        if self.positionals and self.positionals[-1].remaining:
            raise PositionalConflict(
                u("last positional {0} takes all remaining arguments").format(
                    self.positionals[-1]
                )
            )
        self.positionals.append(positional)

    def add_positionals(self, positionals):
        """
        Adds `positionals` from a given iterable.
        """
        for positional in positionals:
            self.add_positional(positional)

    def copy(self):
        return self.__class__()

    def set_parent(self, parent):
        self.parent = parent

    def is_option(self, argument):
        return argument in self.option_shorts or argument in self.option_longs

    def is_command(self, argument):
        return argument in self.commands

    def _print_message(self, message, prefix=None, stream=None):
        if prefix is not None:
            message = u("{0}{1}").format(prefix, message)
        if stream is None:
            stream = self.stdout
        indent = u(" ") * len(prefix) if prefix else u("")
        stream.write(
            u("\n").join(
                textwrap.wrap(
                    message,
                    self.width,
                    subsequent_indent=indent,
                    break_long_words=False
                )
            ) + u("\n")
        )

    def _print_newline(self, stream=None):
        if stream is None:
            stream = self.stdout
        stream.write(u("\n"))

    def print_usage(self, arguments=None):
        self._print_message(self.get_usage(arguments), prefix=u("Usage: "))

    def print_error(self, error):
        self._print_message(error, prefix=u("Error: "), stream=self.stderr)

    def print_help(self, arguments=None):
        self.print_usage(arguments)
        self._print_newline()
        if self.help is not None:
            self._print_message(self.help)
        if self.positionals:
            self._print_positionals_help()
            if self.options or self.commands:
                self._print_newline()
        if self.options:
            self._print_options_help()
            if self.commands:
                self._print_newline()
        if self.commands:
            self._print_commands_help()

    def _print_columns(self, header, rows):
        self._print_message(header)
        usable_width = self.width - self.section_indent
        right_column_length, left_column_length = golden_split(usable_width)
        left_column_length -= 2 # padding
        output = []
        for left, right in rows:
            if right:
                wrapped = textwrap.wrap(
                    right,
                    right_column_length,
                    break_long_words=False
                )
            else:
                wrapped = []
            if len(left) > left_column_length:
                output.append(left)
            else:
                try:
                    first_line = wrapped.pop(0)
                except IndexError:
                    first_line = u("")
                output.append(
                    u("{0}{1}")
                    .format(left.ljust(left_column_length), first_line)
                    .strip()
                )
            output.extend(wrapped)
        self.stdout.write(u("\n").join(
            u("{0}{1}").format(u(" ") * self.section_indent, line)
            for line in output
        ) + u("\n"))

    def _print_positionals_help(self):
        self._print_columns(
            u("Positional Arguments"),
            (
                (positional.metavar, positional.help)
                for positional in self.positionals
            )
        )

    def _print_options_help(self):
        self._print_columns(
            u("Options"),
            (
                (option.get_usage(using="both"), option.help)
                for option in self.options
            )
        )

    def _print_commands_help(self):
        self._print_columns(
            u("Commands"),
            (
                ("{0} {1}".format(name, command.get_usage()), command.help)
                for name, command in self.commands.items()
            )
        )

    def get_match(self, argument):
        modified_argument = argument
        if self.is_command(argument):
            return argument, self.commands[argument], ""
        elif argument in self.option_longs:
            option = self.option_longs[argument]
            return self.options[option], option, ""
        elif argument in self.option_shorts:
            option = self.option_shorts[argument]
            return self.options[option], option, ""
        else:
            modified_argument = argument
            for option, name in self.options.items():
                matched, modified_argument = option.matches(modified_argument)
                if matched:
                    return name, option, modified_argument
        raise UnexpectedArgument(u("{0!r} is unexpected").format(argument))

    def run(self, arguments, default_args=None, default_kwargs=None,
            passthrough_errors=False):
        if not isinstance(arguments, Arguments):
            arguments = Arguments(arguments)
        expected_positionals = iter(self.positionals)
        args = [] if default_args is None else default_args
        kwargs = {}
        if default_kwargs:
            kwargs.update(default_kwargs)
        try:
            for argument in arguments:
                previous_modified = argument
                try:
                    name, match, modified = self.get_match(argument)
                except UnexpectedArgument:
                    exc_info = sys.exc_info()
                    try:
                        positional = next(expected_positionals)
                    except StopIteration:
                        six.reraise(*exc_info)
                    else:
                        arguments.rewind()
                        args = positional.parse_as_positional(
                            self, args, arguments
                        )
                else:
                    while modified != previous_modified:
                        if hasattr(match, "run"):
                            arguments.trace.append([])
                            match.run(arguments, args, kwargs)
                            return
                        kwargs = match.parse(self, kwargs, name, arguments)
                        previous_modified = modified
                        if not modified:
                            break
                        name, option, modified = self.get_match(modified)
            try:
                positional = next(expected_positionals)
            except StopIteration:
                pass
            else:
                if not positional.optional:
                    raise PositionalArgumentMissing(
                        u("expected {positional.metavar}").format(
                            positional=positional
                        )
                    )
        except CLIError:
            if passthrough_errors:
                raise
            self.handle_error(sys.exc_info(), arguments)
            assert False, "exit should have aborted execution"
        return self.main(*args, **kwargs)

    def handle_error(self, exc_info, arguments=None):
        exc_type, exc_value, traceback = exc_info
        try:
            self.stderr
        except AttributeError:
            six.reraise(exc_type, exc_value, traceback)
        self.print_error(exc_value)
        self.print_help(arguments)
        self.exit(exc_value.exit_code)

    def main(self, *args, **kwargs):
        if self.commands:
            # A quick hack to get exc_info for the exception. There is probably
            # a better way to do this.
            try:
                raise CommandMissing(u("expected a command"))
            except CommandMissing:
                self.handle_error(sys.exc_info())
        else:
            raise NotImplementedError(
                "{0}.main(*{1!r}, **{2!r})".format(
                    self.__class__.__name__,
                    args,
                    kwargs
                )
            )