Source code for libqtile.command.base

# Copyright (c) 2019 Sean Vig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the \"Software\"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
The objects in the command graph and command resolution on the objects
"""

import abc
import inspect
import traceback
from typing import Callable, List, Optional, Tuple, Union

from libqtile.command.graph import SelectorType
from libqtile.log_utils import logger


class SelectError(Exception):
    """Error raised in resolving a command graph object"""

    def __init__(self, err_string: str, name: str, selectors: List[SelectorType]):
        super().__init__("{}, name: {}, selectors: {}".format(err_string,
                                                              name,
                                                              selectors))
        self.name = name
        self.selectors = selectors


class CommandError(Exception):
    """Error raised in resolving a command"""


class CommandException(Exception):
    """Error raised while executing a command"""


class CommandObject(metaclass=abc.ABCMeta):
    """Base class for objects that expose commands

    Each command should be a method named `cmd_X`, where X is the command name.
    A CommandObject should also implement `._items()` and `._select()` methods
    (c.f. docstring for `.items()` and `.select()`).
    """

    def select(self, selectors: List[SelectorType]) -> "CommandObject":
        """Return a selected object

        Recursively finds an object specified by a list of `(name, selector)`
        items.

        Raises SelectError if the object does not exist.
        """
        obj = self
        for name, selector in selectors:
            root, items = obj.items(name)
            # if non-root object and no selector given
            if root is False and selector is None:
                raise SelectError("", name, selectors)
            # if no items in container, but selector is given
            if items is None and selector is not None:
                raise SelectError("", name, selectors)
            # if selector is not in the list of contained items
            if items is not None and selector and selector not in items:
                raise SelectError("", name, selectors)

            obj = obj._select(name, selector)
            if obj is None:
                raise SelectError("", name, selectors)
        return obj

    def items(self, name: str) -> Tuple[bool, List[str]]:
        """Build a list of contained items for the given item class

        Returns a tuple `(root, items)` for the specified item class, where:

            root: True if this class accepts a "naked" specification without an
            item seletion (e.g. "layout" defaults to current layout), and False
            if it does not (e.g. no default "widget").

            items: a list of contained items
        """
        ret = self._items(name)
        if ret is None:
            # Not finding information for a particular item class is OK here;
            # we don't expect layouts to have a window, etc.
            return False, []
        return ret

    @abc.abstractmethod
    def _items(self, name) -> Tuple[bool, List[str]]:
        """Generate the items for a given

        Same return as `.items()`. Return `None` if name is not a valid item
        class.
        """

    @abc.abstractmethod
    def _select(self, name: str, sel: Optional[Union[str, int]]) -> "CommandObject":
        """Select the given item of the given item class

        This method is called with the following guarantees:
            - `name` is a valid selector class for this item
            - `sel` is a valid selector for this item
            - the `(name, sel)` tuple is not an "impossible" combination (e.g. a
              selector is specified when `name` is not a containment object).

        Return None if no such object exists
        """

    def command(self, name: str) -> Callable:
        """Return the command with the given name

        Parameters
        ----------
        name : str
            The name of the command to fetch.

        Returns
        -------
        Callable
            The command function that can be invoked.
        """
        return getattr(self, "cmd_" + name, None)

    @property
    def commands(self) -> List[str]:
        """All of the commands on the given object"""
        cmds = [i[4:] for i in dir(self) if i.startswith("cmd_")]
        return cmds

    def cmd_commands(self) -> List[str]:
        """Returns a list of possible commands for this object

        Used by __qsh__ for command completion and online help
        """
        return self.commands

    def cmd_items(self, name) -> Tuple[bool, List[str]]:
        """Returns a list of contained items for the specified name

        Used by __qsh__ to allow navigation of the object graph.
        """
        return self.items(name)

    def cmd_doc(self, name) -> str:
        """Returns the documentation for a specified command name

        Used by __qsh__ to provide online help.
        """
        if name in self.commands:
            command = self.command(name)
            signature = self._get_command_signature(command)
            spec = name + signature
            htext = inspect.getdoc(command) or ""
            return spec + '\n' + htext
        raise CommandError("No such command: %s" % name)

    def _get_command_signature(self, command: Callable) -> str:
        signature = inspect.signature(command)
        args = list(signature.parameters)
        if args and args[0] == "self":
            args = args[1:]
            parameters = [signature.parameters[arg] for arg in args]
            signature = signature.replace(parameters=parameters)
        return str(signature)

    def cmd_eval(self, code: str) -> Tuple[bool, Optional[str]]:
        """Evaluates code in the same context as this function

        Return value is tuple `(success, result)`, success being a boolean and
        result being a string representing the return value of eval, or None if
        exec was used instead.
        """
        try:
            try:
                return True, str(eval(code))
            except SyntaxError:
                exec(code)
                return True, None
        except Exception:
            error = traceback.format_exc().strip().split("\n")[-1]
            return False, error

    def cmd_function(self, function, *args, **kwargs) -> None:
        """Call a function with current object as argument"""
        try:
            function(self, *args, **kwargs)
        except Exception:
            error = traceback.format_exc()
            logger.error('Exception calling "%s":\n%s', function, error)