summaryrefslogtreecommitdiff
path: root/qutebrowser/api/cmdutils.py
blob: 15dfcb310f86a1757d5fb4472b8b041c5f649da3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

"""qutebrowser has the concept of functions, exposed to the user as commands.

Creating a new command is straightforward::

  from qutebrowser.api import cmdutils

  @cmdutils.register(...)
  def foo():
      ...

The commands arguments are automatically deduced by inspecting your function.

The types of the function arguments are inferred based on their default values,
e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline.

The type can be overridden using Python's function annotations::

  @cmdutils.register(...)
  def foo(bar: int, baz=True):
      ...

Possible values:

- A callable (``int``, ``float``, etc.): Gets called to validate/convert the
  value.
- A python enum type: All members of the enum are possible values.
- A ``typing.Union`` of multiple types above: Any of these types are valid
  values, e.g., ``Union[str, int]``.
"""


import inspect
from typing import Any, Callable, Iterable

from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
# pylint: disable=unused-import
from qutebrowser.utils.usertypes import KeyMode, CommandValue as Value


class CommandError(cmdexc.Error):

    """Raised when a command encounters an error while running.

    If your command handler encounters an error and cannot continue, raise this
    exception with an appropriate error message::

        raise cmdexc.CommandError("Message")

    The message will then be shown in the qutebrowser status bar.

    .. note::

       You should only raise this exception while a command handler is run.
       Raising it at another point causes qutebrowser to crash due to an
       unhandled exception.
    """


def check_overflow(arg: int, ctype: str) -> None:
    """Check if the given argument is in bounds for the given type.

    Args:
        arg: The argument to check.
        ctype: The C++/Qt type to check as a string ('int'/'int64').
    """
    try:
        qtutils.check_overflow(arg, ctype)
    except OverflowError:
        raise CommandError("Numeric argument is too large for internal {} "
                           "representation.".format(ctype))


def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None:
    """Check if only one flag is set with exclusive flags.

    Raise a CommandError if not.

    Args:
        flags: The flag values to check.
        names: A list of names (corresponding to the flags argument).
    """
    if sum(1 for e in flags if e) > 1:
        argstr = '/'.join('-' + e for e in names)
        raise CommandError("Only one of {} can be given!".format(argstr))


_CmdHandlerType = Callable[..., Any]


class register:  # noqa: N801,N806 pylint: disable=invalid-name

    """Decorator to register a new command handler."""

    def __init__(self, *,
                 instance: str = None,
                 name: str = None,
                 deprecated_name: str = None,
                 **kwargs: Any) -> None:
        """Save decorator arguments.

        Gets called on parse-time with the decorator arguments.

        Args:
            See class attributes.
        """
        # The object from the object registry to be used as "self".
        self._instance = instance
        # The name of the command
        self._name = name
        # A possible deprecated alias (old name) of the command
        self._deprecated_name = deprecated_name
        # The arguments to pass to Command.
        self._kwargs = kwargs

    def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
        """Register the command before running the function.

        Gets called when a function should be decorated.

        Doesn't actually decorate anything, but creates a Command object and
        registers it in the global commands dict.

        Args:
            func: The function to be decorated.

        Return:
            The original function (unmodified).
        """
        if self._name is None:
            name = func.__name__.lower().replace('_', '-')
        else:
            assert isinstance(self._name, str), self._name
            name = self._name

        cmd = command.Command(
            name=name,
            instance=self._instance,
            handler=func,
            **self._kwargs,
        )
        cmd.register()

        if self._deprecated_name is not None:
            deprecated_cmd = command.Command(
                name=self._deprecated_name,
                instance=self._instance,
                handler=func,
                deprecated=f"use {name} instead",
                **self._kwargs,
            )
            deprecated_cmd.register()

        # This is checked by future @cmdutils.argument calls so they fail
        # (as they'd be silently ignored otherwise)
        func.qute_args = None  # type: ignore[attr-defined]

        return func


class argument:  # noqa: N801,N806 pylint: disable=invalid-name

    """Decorator to customize an argument.

    You can customize how an argument is handled using the
    ``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can,
    for example, be used to customize the flag an argument should get::

      @cmdutils.register(...)
      @cmdutils.argument('bar', flag='c')
      def foo(bar):
          ...

    For a ``str`` argument, you can restrict the allowed strings using
    ``choices``::

      @cmdutils.register(...)
      @cmdutils.argument('bar', choices=['val1', 'val2'])
      def foo(bar: str):
          ...

    For ``Union`` types, the given ``choices`` are only checked if other
    types (like ``int``) don't match.

    The following arguments are supported for ``@cmdutils.argument``:

    - ``flag``: Customize the short flag (``-x``) the argument will get.
    - ``value``: Tell qutebrowser to fill the argument with special values:

      * ``value=cmdutils.Value.count``: The ``count`` given by the user to the
        command.
      * ``value=cmdutils.Value.win_id``: The window ID of the current window.
      * ``value=cmdutils.Value.cur_tab``: The tab object which is currently
        focused.

    - ``completion``: A completion function to use when completing arguments
      for the given command.
    - ``choices``: The allowed string choices for the argument.

    The name of an argument will always be the parameter name, with any
    trailing underscores stripped and underscores replaced by dashes.
    """

    def __init__(self, argname: str, **kwargs: Any) -> None:
        self._argname = argname   # The name of the argument to handle.
        self._kwargs = kwargs  # Valid ArgInfo members.

    def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType:
        funcname = func.__name__

        if self._argname not in inspect.signature(func).parameters:
            raise ValueError("{} has no argument {}!".format(funcname,
                                                             self._argname))
        if not hasattr(func, 'qute_args'):
            func.qute_args = {}  # type: ignore[attr-defined]
        elif func.qute_args is None:
            raise ValueError("@cmdutils.argument got called above (after) "
                             "@cmdutils.register for {}!".format(funcname))

        arginfo = command.ArgInfo(**self._kwargs)
        func.qute_args[self._argname] = arginfo  # type: ignore[attr-defined]

        return func