qutebrowser/qutebrowser/commands/command.py

527 lines
21 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2016-01-04 07:12:39 +01:00
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-02-06 14:01:23 +01:00
#
# 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 <http://www.gnu.org/licenses/>.
2014-01-29 15:30:19 +01:00
2014-02-17 12:23:52 +01:00
"""Contains the Command class, a skeleton for a command."""
import inspect
import collections
import traceback
2014-09-02 21:54:07 +02:00
from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg,
usertypes, typing)
from qutebrowser.utils import debug as debug_utils
2014-01-29 06:28:21 +01:00
2014-03-03 21:06:10 +01:00
class ArgInfo:
"""Information about an argument."""
2016-05-10 20:40:39 +02:00
def __init__(self, win_id=False, count=False, flag=None, hide=False,
metavar=None, completion=None, choices=None):
if win_id and count:
raise TypeError("Argument marked as both count/win_id!")
self.win_id = win_id
self.count = count
self.flag = flag
self.hide = hide
2016-05-10 20:40:39 +02:00
self.metavar = metavar
2016-05-10 23:04:52 +02:00
self.completion = completion
self.choices = choices
def __eq__(self, other):
return (self.win_id == other.win_id and
self.count == other.count and
self.flag == other.flag and
2016-05-10 20:40:39 +02:00
self.hide == other.hide and
2016-05-10 23:04:52 +02:00
self.metavar == other.metavar and
self.completion == other.completion and
self.choices == other.choices)
def __repr__(self):
return utils.get_repr(self, win_id=self.win_id, count=self.count,
2016-05-10 20:40:39 +02:00
flag=self.flag, hide=self.hide,
2016-05-10 23:04:52 +02:00
metavar=self.metavar, completion=self.completion,
choices=self.choices, constructor=True)
class Command:
2014-02-07 20:21:50 +01:00
2014-01-29 15:30:19 +01:00
"""Base skeleton for a command.
2014-03-03 06:09:23 +01:00
Attributes:
2014-03-04 07:02:45 +01:00
name: The main name of the command.
maxsplit: The maximum amount of splits to do for the commandline, or
None.
2014-03-04 07:02:45 +01:00
hide: Whether to hide the arguments or not.
deprecated: False, or a string to describe why a command is deprecated.
2014-03-04 07:02:45 +01:00
desc: The description of the command.
handler: The handler function to call.
2014-06-16 09:44:11 +02:00
debug: Whether this is a debugging command (only shown with --debug).
2014-09-02 21:54:07 +02:00
parser: The ArgumentParser to use to parse this command.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
backend: Which backend the command works with (or None if it works with
both)
no_replace_variables: Don't replace variables like {url}
_qute_args: The saved data from @cmdutils.argument
_modes: The modes the command can be executed in.
_not_modes: The modes the command can not be executed in.
_count: The count set for the command.
_instance: The object to bind 'self' to.
_scope: The scope to get _instance for in the object registry.
2014-01-29 06:28:21 +01:00
"""
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, debug=False,
ignore_args=False, deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', backend=None,
no_replace_variables=False):
2014-04-25 11:21:00 +02:00
# I really don't know how to solve this in a better way, I tried.
2015-12-01 20:55:38 +01:00
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None:
raise ValueError("Only modes or not_modes can be given!")
if modes is not None:
for m in modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
if not_modes is not None:
for m in not_modes:
if not isinstance(m, usertypes.KeyMode):
raise TypeError("Mode {} is no KeyMode member!".format(m))
if scope != 'global' and instance is None:
raise ValueError("Setting scope without setting instance makes "
"no sense!")
2014-02-28 17:00:25 +01:00
self.name = name
self.maxsplit = maxsplit
2014-02-28 17:00:25 +01:00
self.hide = hide
self.deprecated = deprecated
self._instance = instance
self._modes = modes
self._not_modes = not_modes
self._scope = scope
self._star_args_optional = star_args_optional
self.debug = debug
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.backend = backend
self.no_replace_variables = no_replace_variables
2014-09-23 04:22:51 +02:00
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
epilog=self.docparser.long_desc)
self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS)
self._check_func()
self.opt_args = collections.OrderedDict()
2014-09-15 08:16:19 +02:00
self.namespace = None
self._count = None
self.pos_args = []
2014-10-14 07:59:42 +02:00
self.desc = None
self.flags_with_args = []
# This is checked by future @cmdutils.argument calls so they fail
# (as they'd be silently ignored otherwise)
self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None
self._inspect_func()
2014-01-29 06:28:21 +01:00
2014-09-28 22:13:14 +02:00
def _check_prerequisites(self, win_id):
2014-09-02 21:54:07 +02:00
"""Check if the command is permitted to run currently.
2014-02-19 10:58:32 +01:00
2014-09-28 22:13:14 +02:00
Args:
win_id: The window ID the command is run in.
2014-01-29 06:28:21 +01:00
"""
2014-09-28 22:13:14 +02:00
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)
curmode = mode_manager.mode
if self._modes is not None and curmode not in self._modes:
mode_names = '/'.join(mode.name for mode in self._modes)
2014-08-26 19:10:14 +02:00
raise cmdexc.PrerequisitesError(
"{}: This command is only allowed in {} mode.".format(
self.name, mode_names))
elif self._not_modes is not None and curmode in self._not_modes:
mode_names = '/'.join(mode.name for mode in self._not_modes)
2014-08-26 19:10:14 +02:00
raise cmdexc.PrerequisitesError(
"{}: This command is not allowed in {} mode.".format(
self.name, mode_names))
2016-08-03 11:35:08 +02:00
used_backend = usertypes.arg2backend[objreg.get('args').backend]
if self.backend is not None and used_backend != self.backend:
raise cmdexc.PrerequisitesError(
"{}: Only available with {} "
"backend.".format(self.name, self.backend.name))
if self.deprecated:
2016-09-14 20:52:32 +02:00
message.warning('{} is deprecated - {}'.format(self.name,
self.deprecated))
2014-01-29 06:28:21 +01:00
def _check_func(self):
"""Make sure the function parameters don't violate any rules."""
signature = inspect.signature(self.handler)
if 'self' in signature.parameters and self._instance is None:
raise TypeError("{} is a class method, but instance was not "
"given!".format(self.name[0]))
elif 'self' not in signature.parameters and self._instance is not None:
raise TypeError("{} is not a class method, but instance was "
"given!".format(self.name[0]))
elif any(param.kind == inspect.Parameter.VAR_KEYWORD
for param in signature.parameters.values()):
raise TypeError("{}: functions with varkw arguments are not "
"supported!".format(self.name[0]))
2016-05-10 20:26:54 +02:00
def get_arg_info(self, param):
"""Get an ArgInfo tuple for the given inspect.Parameter."""
return self._qute_args.get(param.name, ArgInfo())
def get_pos_arg_info(self, pos):
"""Get an ArgInfo tuple for the given positional parameter."""
name = self.pos_args[pos][0]
return self._qute_args.get(name, ArgInfo())
def _inspect_special_param(self, param):
2014-10-14 07:59:42 +02:00
"""Check if the given parameter is a special one.
Args:
param: The inspect.Parameter to handle.
Return:
True if the parameter is special, False otherwise.
"""
2016-05-10 20:26:54 +02:00
arg_info = self.get_arg_info(param)
if arg_info.count:
2014-10-14 07:59:42 +02:00
if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter "
"without default!".format(self.name))
return True
elif arg_info.win_id:
2014-10-14 07:59:42 +02:00
return True
def _inspect_func(self):
"""Inspect the function to get useful informations from it.
2014-10-14 07:59:42 +02:00
Sets instance attributes (desc, type_conv, name_conv) based on the
informations.
Return:
How many user-visible arguments the command has.
"""
signature = inspect.signature(self.handler)
doc = inspect.getdoc(self.handler)
if doc is not None:
2014-10-14 07:59:42 +02:00
self.desc = doc.splitlines()[0].strip()
else:
2014-10-14 07:59:42 +02:00
self.desc = ""
if not self.ignore_args:
for param in signature.parameters.values():
# https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
# "Python has no explicit syntax for defining positional-only
# parameters, but many built-in and extension module functions
# (especially those that accept only one or two parameters)
# accept them."
assert param.kind != inspect.Parameter.POSITIONAL_ONLY
if param.name == 'self':
continue
if self._inspect_special_param(param):
continue
if (param.kind == inspect.Parameter.KEYWORD_ONLY and
param.default is inspect.Parameter.empty):
raise TypeError("{}: handler has keyword only argument "
"without default!".format(self.name))
2016-05-10 22:03:10 +02:00
typ = self._get_type(param)
is_bool = typ is bool
kwargs = self._param_to_argparse_kwargs(param, is_bool)
args = self._param_to_argparse_args(param, is_bool)
callsig = debug_utils.format_call(
self.parser.add_argument, args, kwargs,
full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
2016-05-10 23:04:52 +02:00
return signature.parameters.values()
def _param_to_argparse_kwargs(self, param, is_bool):
2014-10-26 22:08:13 +01:00
"""Get argparse keyword arguments for a parameter.
Args:
param: The inspect.Parameter object to get the args for.
is_bool: Whether the parameter is a boolean.
2014-10-26 22:08:13 +01:00
Return:
A kwargs dict.
"""
kwargs = {}
try:
kwargs['help'] = self.docparser.arg_descs[param.name]
except KeyError:
pass
kwargs['dest'] = param.name
2016-05-10 20:40:39 +02:00
arg_info = self.get_arg_info(param)
if is_bool:
kwargs['action'] = 'store_true'
else:
if arg_info.metavar is not None:
kwargs['metavar'] = arg_info.metavar
else:
kwargs['metavar'] = argparser.arg_name(param.name)
if param.kind == inspect.Parameter.VAR_POSITIONAL:
kwargs['nargs'] = '*' if self._star_args_optional else '+'
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs['default'] = param.default
elif not is_bool and param.default is not inspect.Parameter.empty:
kwargs['default'] = param.default
kwargs['nargs'] = '?'
2014-10-26 22:08:13 +01:00
return kwargs
def _param_to_argparse_args(self, param, is_bool):
2014-10-26 22:08:13 +01:00
"""Get argparse positional arguments for a parameter.
Args:
param: The inspect.Parameter object to get the args for.
is_bool: Whether the parameter is a boolean.
2014-10-26 22:08:13 +01:00
Return:
A list of args.
"""
args = []
name = argparser.arg_name(param.name)
2016-05-10 20:26:54 +02:00
arg_info = self.get_arg_info(param)
if arg_info.flag is not None:
shortname = arg_info.flag
else:
shortname = name[0]
if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
"exactly 1 char!".format(shortname, name,
self.name))
if is_bool or param.kind == inspect.Parameter.KEYWORD_ONLY:
long_flag = '--{}'.format(name)
short_flag = '-{}'.format(shortname)
args.append(long_flag)
args.append(short_flag)
self.opt_args[param.name] = long_flag, short_flag
if not is_bool:
self.flags_with_args += [short_flag, long_flag]
2014-10-26 22:08:13 +01:00
else:
if not arg_info.hide:
self.pos_args.append((param.name, name))
2014-10-26 22:08:13 +01:00
return args
2016-05-10 22:03:10 +02:00
def _get_type(self, param):
"""Get the type of an argument from its default value or annotation.
Args:
param: The inspect.Parameter to look at.
"""
arginfo = self.get_arg_info(param)
2016-05-10 22:03:10 +02:00
if param.annotation is not inspect.Parameter.empty:
return param.annotation
elif param.default not in [None, inspect.Parameter.empty]:
return type(param.default)
elif arginfo.count or arginfo.win_id or param.kind in [
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD]:
return None
else:
return str
2014-09-28 22:13:14 +02:00
def _get_self_arg(self, win_id, param, args):
2014-09-15 08:16:19 +02:00
"""Get the self argument for a function call.
2014-05-14 18:00:40 +02:00
2014-09-15 08:16:19 +02:00
Arguments:
2014-09-28 22:13:14 +02:00
win_id: The window id this command should be executed in.
2014-09-15 08:16:19 +02:00
param: The count parameter.
args: The positional argument list. Gets modified directly.
"""
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
if self._scope == 'global':
tab_id = None
2014-09-28 22:13:14 +02:00
win_id = None
elif self._scope == 'tab':
tab_id = 'current'
elif self._scope == 'window':
tab_id = None
else:
raise ValueError("Invalid scope {}!".format(self._scope))
obj = objreg.get(self._instance, scope=self._scope, window=win_id,
tab=tab_id)
2014-09-15 08:16:19 +02:00
args.append(obj)
def _get_count_arg(self, param, args, kwargs):
"""Add the count argument to a function call.
Arguments:
param: The count parameter.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
if self._count is not None:
args.append(self._count)
2014-09-15 08:16:19 +02:00
else:
args.append(param.default)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if self._count is not None:
kwargs[param.name] = self._count
2014-09-15 08:16:19 +02:00
else:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name))
2014-09-15 08:16:19 +02:00
def _get_win_id_arg(self, win_id, param, args, kwargs):
"""Add the win_id argument to a function call.
Arguments:
win_id: The window ID to add.
param: The count parameter.
args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly.
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(win_id)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[param.name] = win_id
else:
raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name))
def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter."""
value = getattr(self.namespace, param.name)
typ = self._get_type(param)
if isinstance(typ, tuple):
raise TypeError("{}: Legacy tuple type annotation!".format(
self.name))
elif issubclass(typ, typing.Union):
# this is... slightly evil, I know
2016-08-26 05:21:10 +02:00
types = list(typ.__union_params__) # pylint: disable=no-member
if param.default is not inspect.Parameter.empty:
types.append(type(param.default))
choices = self.get_arg_info(param).choices
value = argparser.multitype_conv(param, types, value,
str_choices=choices)
elif typ is str:
choices = self.get_arg_info(param).choices
value = argparser.type_conv(param, typ, value, str_choices=choices)
elif typ is bool: # no type conversion for flags
assert isinstance(value, bool)
elif typ is None:
pass
else:
value = argparser.type_conv(param, typ, value)
return value
2014-09-15 08:16:19 +02:00
def _get_call_args(self, win_id):
2014-09-15 08:16:19 +02:00
"""Get arguments for a function call.
2014-09-14 23:56:19 +02:00
2014-09-28 22:13:14 +02:00
Args:
win_id: The window id this command should be executed in.
2014-09-14 23:56:19 +02:00
Return:
An (args, kwargs) tuple.
2014-01-29 06:28:21 +01:00
"""
2014-09-14 23:56:19 +02:00
args = []
kwargs = {}
2014-09-15 08:16:19 +02:00
signature = inspect.signature(self.handler)
if self.ignore_args:
if self._instance is not None:
param = list(signature.parameters.values())[0]
2014-09-28 22:13:14 +02:00
self._get_self_arg(win_id, param, args)
return args, kwargs
for i, param in enumerate(signature.parameters.values()):
2016-05-10 20:26:54 +02:00
arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None:
# Special case for 'self'.
2014-09-28 22:13:14 +02:00
self._get_self_arg(win_id, param, args)
continue
elif arg_info.count:
# Special case for count parameter.
2014-09-15 08:16:19 +02:00
self._get_count_arg(param, args, kwargs)
continue
# elif arg_info.win_id:
elif arg_info.win_id:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
continue
value = self._get_param_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
2014-09-14 23:56:19 +02:00
args.append(value)
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
if value is not None:
2014-09-14 23:56:19 +02:00
args += value
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[param.name] = value
else:
raise TypeError("{}: Invalid parameter type {} for argument "
"'{}'!".format(
self.name, param.kind, param.name))
2014-09-14 23:56:19 +02:00
return args, kwargs
2014-09-28 22:13:14 +02:00
def run(self, win_id, args=None, count=None):
2014-09-14 23:56:19 +02:00
"""Run the command.
Note we don't catch CommandError here as it might happen async.
Args:
2014-09-28 22:13:14 +02:00
win_id: The window ID the command is run in.
2014-09-14 23:56:19 +02:00
args: Arguments to the command.
count: Command repetition count.
"""
dbgout = ["command called:", self.name]
if args:
dbgout.append(str(args))
2016-07-08 20:13:35 +02:00
elif args is None:
args = []
2014-09-14 23:56:19 +02:00
if count is not None:
dbgout.append("(count={})".format(count))
log.commands.debug(' '.join(dbgout))
try:
2014-09-15 08:16:19 +02:00
self.namespace = self.parser.parse_args(args)
2014-09-14 23:56:19 +02:00
except argparser.ArgumentParserError as e:
2016-09-14 20:52:32 +02:00
message.error('{}: {}'.format(self.name, e),
stack=traceback.format_exc())
2014-09-14 23:56:19 +02:00
return
except argparser.ArgumentParserExit as e:
log.commands.debug("argparser exited with status {}: {}".format(
e.status, e))
return
self._count = count
2014-09-28 22:13:14 +02:00
self._check_prerequisites(win_id)
posargs, kwargs = self._get_call_args(win_id)
2014-09-03 08:57:21 +02:00
log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs)))
2014-09-02 21:54:07 +02:00
self.handler(*posargs, **kwargs)