Lots of fixes for new command system.
Squashed commit: - Fix getting current URL - Get rid of *args for hints. - Make enums work. - Fix moving commands to utilcmds. - Fix enums in argparse - Fix arg splitting for hints. - Fix default enum args. - Fix argument splitting for hints if None is given. - Fix set_cmd_text with flags and fix {url}. - Fix unittests - Fix tuple types for arguments. - Fix scroll-page. - Fix lint - Fix open_target. - Others
This commit is contained in:
parent
d836e26107
commit
57d51ad9bb
3
.flake8
3
.flake8
@ -11,7 +11,8 @@
|
||||
# E222: Multiple spaces after operator
|
||||
# F811: Redifiniton
|
||||
# W292: No newline at end of file
|
||||
# E701: multiple statements on one line
|
||||
# E702: multiple statements on one line
|
||||
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E702
|
||||
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702
|
||||
max_complexity = 12
|
||||
exclude = ez_setup.py
|
||||
|
@ -233,7 +233,7 @@ class CommandDispatcher:
|
||||
else:
|
||||
diag = QPrintDialog()
|
||||
diag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
diag.open(lambda: tab.print(printdiag.printer()))
|
||||
diag.open(lambda: tab.print(diag.printer()))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def back(self, count=1):
|
||||
@ -257,7 +257,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def hint(self, group=webelem.Group.all, target=hints.Target.normal,
|
||||
*args : {'nargs': '*'}):
|
||||
args=None):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
@ -286,7 +286,7 @@ class CommandDispatcher:
|
||||
link.
|
||||
- `spawn`: Spawn a command.
|
||||
|
||||
*args: Arguments for spawn/userscript/fill.
|
||||
args: Arguments for spawn/userscript/fill.
|
||||
|
||||
- With `spawn`: The executable and arguments to spawn.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
@ -301,7 +301,7 @@ class CommandDispatcher:
|
||||
if frame is None:
|
||||
raise cmdexc.CommandError("No frame focused!")
|
||||
widget.hintmanager.start(frame, self._tabs.current_url(), group,
|
||||
target, *args)
|
||||
target, args)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def follow_hint(self):
|
||||
@ -364,7 +364,7 @@ class CommandDispatcher:
|
||||
Qt.Horizontal if horizontal else Qt.Vertical)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
|
||||
def scroll_page(self, x : int, y : int, count=1):
|
||||
def scroll_page(self, x: float, y: float, count=1):
|
||||
"""Scroll the frame page-wise.
|
||||
|
||||
Args:
|
||||
@ -402,7 +402,8 @@ class CommandDispatcher:
|
||||
target = "clipboard"
|
||||
log.misc.debug("Yanking to {}: '{}'".format(target, s))
|
||||
clipboard.setText(s, mode)
|
||||
message.info("URL yanked to {}".format(target))
|
||||
what = 'Title' if title else 'URL'
|
||||
message.info("{} yanked to {}".format(what, target))
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def zoom_in(self, count=1):
|
||||
@ -518,7 +519,7 @@ class CommandDispatcher:
|
||||
widget.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='mainwindow.tabs.cmd')
|
||||
def tab_focus(self, index : int = None, count=None):
|
||||
def tab_focus(self, index: (int, 'last')=None, count=None):
|
||||
"""Select the tab given as argument/[count].
|
||||
|
||||
Args:
|
||||
|
@ -20,6 +20,7 @@
|
||||
"""A HintManager to draw hints over links."""
|
||||
|
||||
import math
|
||||
import shlex
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
@ -472,7 +473,7 @@ class HintManager(QObject):
|
||||
self.openurl.emit(url, newtab)
|
||||
|
||||
def start(self, mainframe, baseurl, group=webelem.Group.all,
|
||||
target=Target.normal, *args):
|
||||
target=Target.normal, args=None):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
@ -480,7 +481,7 @@ class HintManager(QObject):
|
||||
baseurl: URL of the current page.
|
||||
group: Which group of elements to hint.
|
||||
target: What to do with the link. See attribute docstring.
|
||||
*args: Arguments for userscript/download
|
||||
args: Arguments for userscript/download
|
||||
|
||||
Emit:
|
||||
hint_strings_updated: Emitted to update keypraser.
|
||||
@ -493,14 +494,13 @@ class HintManager(QObject):
|
||||
# on_mode_left, we are extra careful here.
|
||||
raise ValueError("start() was called with frame=None")
|
||||
if target in (Target.userscript, Target.spawn, Target.fill):
|
||||
if not args:
|
||||
if args is None:
|
||||
raise cmdexc.CommandError(
|
||||
"Additional arguments are required with target "
|
||||
"userscript/spawn/fill.")
|
||||
"'args' is required with target userscript/spawn/fill.")
|
||||
else:
|
||||
if args:
|
||||
if args is not None:
|
||||
raise cmdexc.CommandError(
|
||||
"Arguments are only allowed with target userscript/spawn.")
|
||||
"'args' is only allowed with target userscript/spawn.")
|
||||
elems = []
|
||||
ctx = HintContext()
|
||||
ctx.frames = webelem.get_child_frames(mainframe)
|
||||
@ -514,7 +514,13 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
ctx.target = target
|
||||
ctx.baseurl = baseurl
|
||||
ctx.args = args
|
||||
if args is None:
|
||||
ctx.args = None
|
||||
else:
|
||||
try:
|
||||
ctx.args = shlex.split(args)
|
||||
except ValueError as e:
|
||||
raise cmdexc.CommandError("Could not split args: {}".format(e))
|
||||
message.instance().set_text(self.HINT_TEXTS[target])
|
||||
strings = self._hint_strings(visible_elems)
|
||||
for e, string in zip(visible_elems, strings):
|
||||
|
@ -19,8 +19,12 @@
|
||||
|
||||
"""argparse.ArgumentParser subclass to parse qutebrowser commands."""
|
||||
|
||||
|
||||
import argparse
|
||||
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
class ArgumentParserError(Exception):
|
||||
|
||||
@ -29,6 +33,8 @@ class ArgumentParserError(Exception):
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
"""Subclass ArgumentParser to be more suitable for runtime parsing."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(add_help=False)
|
||||
|
||||
@ -37,4 +43,46 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
'Status: {}, message: {}'.format(status, msg))
|
||||
|
||||
def error(self, msg):
|
||||
raise ArgumentParserError(msg)
|
||||
raise ArgumentParserError(msg[0].upper() + msg[1:])
|
||||
|
||||
|
||||
def enum_getter(enum):
|
||||
"""Function factory to get an enum getter."""
|
||||
|
||||
def _get_enum_item(key):
|
||||
"""Helper function to get an enum item.
|
||||
|
||||
Passes through existing items unmodified.
|
||||
"""
|
||||
if isinstance(key, enum):
|
||||
return key
|
||||
try:
|
||||
return enum[key.replace('-', '_')]
|
||||
except KeyError:
|
||||
raise cmdexc.ArgumentTypeError("Invalid value {}.".format(key))
|
||||
|
||||
return _get_enum_item
|
||||
|
||||
|
||||
def multitype_conv(tpl):
|
||||
"""Function factory to get a type converter for a choice of types."""
|
||||
|
||||
def _convert(value):
|
||||
"""Convert a value according to an iterable of possible arg types."""
|
||||
for typ in tpl:
|
||||
if isinstance(typ, str):
|
||||
if value == typ:
|
||||
return value
|
||||
elif utils.is_enum(typ):
|
||||
return enum_getter(typ)(value)
|
||||
elif callable(typ):
|
||||
# int, float, etc.
|
||||
if isinstance(value, typ):
|
||||
return value
|
||||
try:
|
||||
return typ(value)
|
||||
except ValueError:
|
||||
pass
|
||||
raise cmdexc.ArgumentTypeError('Invalid value {}.'.format(value))
|
||||
|
||||
return _convert
|
||||
|
@ -49,6 +49,13 @@ class ArgumentCountError(CommandMetaError):
|
||||
pass
|
||||
|
||||
|
||||
class ArgumentTypeError(CommandMetaError):
|
||||
|
||||
"""Raised when an argument had an invalid type."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PrerequisitesError(CommandMetaError):
|
||||
|
||||
"""Raised when a cmd can't be used because some prerequisites aren't met.
|
||||
|
@ -23,11 +23,11 @@ Module attributes:
|
||||
cmd_dict: A mapping from command-strings to command objects.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import inspect
|
||||
import collections
|
||||
|
||||
from qutebrowser.utils import usertypes, qtutils, log, debug
|
||||
from qutebrowser.utils import usertypes, qtutils, log, utils
|
||||
from qutebrowser.utils import debug as debugutils
|
||||
from qutebrowser.commands import command, cmdexc, argparser
|
||||
|
||||
cmd_dict = {}
|
||||
@ -160,15 +160,16 @@ class register: # pylint: disable=invalid-name
|
||||
"""
|
||||
names = self._get_names(func)
|
||||
log.commands.vdebug("Registering command {}".format(names[0]))
|
||||
if any(name in cmd_dict for name in names):
|
||||
for name in names:
|
||||
if name in cmd_dict:
|
||||
raise ValueError("{} is already registered!".format(name))
|
||||
has_count, desc, parser = self._inspect_func(func)
|
||||
has_count, desc, parser, type_conv = self._inspect_func(func)
|
||||
cmd = command.Command(
|
||||
name=names[0], split=self.split, hide=self.hide, count=has_count,
|
||||
desc=desc, instance=self.instance, handler=func,
|
||||
completion=self.completion, modes=self.modes,
|
||||
not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug,
|
||||
parser=parser)
|
||||
not_modes=self.not_modes, needs_js=self.needs_js,
|
||||
is_debug=self.debug, parser=parser, type_conv=type_conv)
|
||||
for name in names:
|
||||
cmd_dict[name] = cmd
|
||||
return func
|
||||
@ -202,15 +203,17 @@ class register: # pylint: disable=invalid-name
|
||||
func: The function to look at.
|
||||
|
||||
Return:
|
||||
A (has_count, desc, parser) tuple.
|
||||
A (has_count, desc, parser, type_conv) tuple.
|
||||
has_count: Whether the command supports a count.
|
||||
desc: The description of the command.
|
||||
parser: The ArgumentParser to use when parsing the commandline.
|
||||
type_conv: A mapping of args to type converter callables.
|
||||
"""
|
||||
type_conv = {}
|
||||
signature = inspect.signature(func)
|
||||
if 'self' in signature.parameters and self.instance is None:
|
||||
raise ValueError("{} is a class method, but instance was not "
|
||||
"given!".format(mainname))
|
||||
"given!".format(self.name[0]))
|
||||
has_count = 'count' in signature.parameters
|
||||
parser = argparser.ArgumentParser()
|
||||
if func.__doc__ is not None:
|
||||
@ -224,43 +227,64 @@ class register: # pylint: disable=invalid-name
|
||||
args = []
|
||||
kwargs = {}
|
||||
annotation_info = self._parse_annotation(param)
|
||||
if annotation_info.typ is not None:
|
||||
typ = annotation_info.typ
|
||||
else:
|
||||
typ = self._infer_type(param)
|
||||
kwargs.update(self._type_to_argparse(typ))
|
||||
kwargs.update(self._param_to_argparse_kw(
|
||||
param, annotation_info))
|
||||
kwargs.update(annotation_info.kwargs)
|
||||
if (param.kind == inspect.Parameter.VAR_POSITIONAL and
|
||||
'nargs' not in kwargs): # annotation_info overrides it
|
||||
kwargs['nargs'] = '*'
|
||||
is_flag = typ == bool
|
||||
args += self._get_argparse_args(param, annotation_info,
|
||||
is_flag)
|
||||
callsig = debug.format_call(parser.add_argument, args,
|
||||
args += self._param_to_argparse_pos(param, annotation_info)
|
||||
typ = self._get_type(param, annotation_info)
|
||||
if utils.is_enum(typ):
|
||||
type_conv[param.name] = argparser.enum_getter(typ)
|
||||
elif isinstance(typ, tuple):
|
||||
type_conv[param.name] = argparser.multitype_conv(typ)
|
||||
callsig = debugutils.format_call(parser.add_argument, args,
|
||||
kwargs, full=False)
|
||||
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
|
||||
param.name, typ, callsig))
|
||||
parser.add_argument(*args, **kwargs)
|
||||
return has_count, desc, parser
|
||||
return has_count, desc, parser, type_conv
|
||||
|
||||
def _get_argparse_args(self, param, annotation_info, is_flag):
|
||||
def _param_to_argparse_pos(self, param, annotation_info):
|
||||
"""Get a list of positional argparse arguments.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter instance for the current parameter.
|
||||
annotation_info: An AnnotationInfo tuple for the parameter.
|
||||
is_flag: Whether the option is a flag or not.
|
||||
"""
|
||||
args = []
|
||||
name = annotation_info.name or param.name
|
||||
shortname = annotation_info.flag or param.name[0]
|
||||
if is_flag:
|
||||
if self._get_type(param, annotation_info) == bool:
|
||||
args.append('--{}'.format(name))
|
||||
args.append('-{}'.format(shortname))
|
||||
else:
|
||||
args.append(name)
|
||||
return args
|
||||
|
||||
def _param_to_argparse_kw(self, param, annotation_info):
|
||||
"""Get argparse keyword arguments for a parameter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter object to get the args for.
|
||||
annotation_info: An AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
kwargs = {}
|
||||
typ = self._get_type(param, annotation_info)
|
||||
if isinstance(typ, tuple):
|
||||
pass
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
kwargs['type'] = typ
|
||||
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
kwargs['nargs'] = '*'
|
||||
elif typ is not bool and param.default is not inspect.Parameter.empty:
|
||||
kwargs['default'] = param.default
|
||||
kwargs['nargs'] = '?'
|
||||
return kwargs
|
||||
|
||||
def _parse_annotation(self, param):
|
||||
"""Get argparse arguments and type from a parameter annotation.
|
||||
|
||||
@ -276,8 +300,9 @@ class register: # pylint: disable=invalid-name
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None}
|
||||
log.commands.vdebug("Parsing annotation {}".format(param.annotation))
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
if isinstance(param.annotation, dict):
|
||||
for field in ('type', 'flag', 'name'):
|
||||
if field in param.annotation:
|
||||
@ -288,27 +313,16 @@ class register: # pylint: disable=invalid-name
|
||||
info['typ'] = param.annotation
|
||||
return self.AnnotationInfo(**info)
|
||||
|
||||
def _infer_type(self, param):
|
||||
"""Get the type of an argument from its default value."""
|
||||
if param.default is None or param.default is inspect.Parameter.empty:
|
||||
def _get_type(self, param, annotation_info):
|
||||
"""Get the type of an argument from its default value or annotation.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to look at.
|
||||
annotation_info: An AnnotationInfo tuple which overrides the type.
|
||||
"""
|
||||
if annotation_info.typ is not None:
|
||||
return annotation_info.typ
|
||||
elif param.default is None or param.default is inspect.Parameter.empty:
|
||||
return None
|
||||
else:
|
||||
return type(param.default)
|
||||
|
||||
def _type_to_argparse(self, typ):
|
||||
"""Get argparse keyword arguments based on a type."""
|
||||
kwargs = {}
|
||||
try:
|
||||
is_enum = issubclass(typ, enum.Enum)
|
||||
except TypeError:
|
||||
is_enum = False
|
||||
if isinstance(typ, tuple):
|
||||
kwargs['choices'] = typ
|
||||
elif is_enum:
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
kwargs['type'] = typ
|
||||
|
||||
return kwargs
|
||||
|
@ -43,6 +43,7 @@ class Command:
|
||||
needs_js: Whether the command needs javascript enabled
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
type_conv: A mapping of conversion functions for arguments.
|
||||
"""
|
||||
|
||||
# TODO:
|
||||
@ -50,7 +51,8 @@ class Command:
|
||||
# this might be combined with help texts or so as well
|
||||
|
||||
def __init__(self, name, split, hide, count, desc, instance, handler,
|
||||
completion, modes, not_modes, needs_js, debug, parser):
|
||||
completion, modes, not_modes, needs_js, is_debug, parser,
|
||||
type_conv):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-arguments
|
||||
self.name = name
|
||||
@ -64,8 +66,9 @@ class Command:
|
||||
self.modes = modes
|
||||
self.not_modes = not_modes
|
||||
self.needs_js = needs_js
|
||||
self.debug = debug
|
||||
self.debug = is_debug
|
||||
self.parser = parser
|
||||
self.type_conv = type_conv
|
||||
|
||||
def _check_prerequisites(self):
|
||||
"""Check if the command is permitted to run currently.
|
||||
@ -114,7 +117,7 @@ class Command:
|
||||
try:
|
||||
namespace = self.parser.parse_args(args)
|
||||
except argparser.ArgumentParserError as e:
|
||||
message.error(str(e))
|
||||
message.error('{}: {}'.format(self.name, e))
|
||||
return
|
||||
|
||||
for name, arg in vars(namespace).items():
|
||||
@ -124,6 +127,12 @@ class Command:
|
||||
# FIXME: This approach is rather naive, but for now it works.
|
||||
posargs += arg
|
||||
else:
|
||||
if name in self.type_conv:
|
||||
# We convert enum types after getting the values from
|
||||
# argparse, because argparse's choices argument is
|
||||
# processed after type conversation, which is not what we
|
||||
# want.
|
||||
arg = self.type_conv[name](arg)
|
||||
kwargs[name] = arg
|
||||
|
||||
if self.instance is not None:
|
||||
@ -140,4 +149,7 @@ class Command:
|
||||
self._check_prerequisites()
|
||||
log.commands.debug('Calling {}'.format(
|
||||
debug.format_call(self.handler, posargs, kwargs)))
|
||||
# FIXME this won't work properly if some arguments are required to be
|
||||
# positional, e.g.:
|
||||
# def fun(one=True, two=False, *args)
|
||||
self.handler(*posargs, **kwargs)
|
||||
|
@ -27,6 +27,20 @@ from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, log, utils
|
||||
|
||||
|
||||
def replace_variables(arglist):
|
||||
"""Utility function to replace variables like {url} in a list of args."""
|
||||
args = []
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
app = QCoreApplication.instance()
|
||||
url = app.mainwindow.tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args.append(url)
|
||||
else:
|
||||
args.append(arg)
|
||||
return args
|
||||
|
||||
|
||||
class SearchRunner(QObject):
|
||||
|
||||
"""Run searches on webpages.
|
||||
@ -219,6 +233,14 @@ class CommandRunner:
|
||||
else:
|
||||
raise cmdexc.NoSuchCommandError(
|
||||
'{}: no such command'.format(cmdstr))
|
||||
self._split_args(argstr)
|
||||
retargs = self._args[:]
|
||||
if text.endswith(' '):
|
||||
retargs.append('')
|
||||
return [cmdstr] + retargs
|
||||
|
||||
def _split_args(self, argstr):
|
||||
"""Split the arguments from an arg string."""
|
||||
if argstr is None:
|
||||
self._args = []
|
||||
elif self._cmd.split:
|
||||
@ -244,10 +266,6 @@ class CommandRunner:
|
||||
# If there are only flags, we got it right on the first try
|
||||
# already.
|
||||
self._args = split_args
|
||||
retargs = self._args[:]
|
||||
if text.endswith(' '):
|
||||
retargs.append('')
|
||||
return [cmdstr] + retargs
|
||||
|
||||
def run(self, text, count=None):
|
||||
"""Parse a command from a line of text and run it.
|
||||
@ -261,15 +279,11 @@ class CommandRunner:
|
||||
self.run(sub, count)
|
||||
return
|
||||
self.parse(text)
|
||||
app = QCoreApplication.instance()
|
||||
cur_url = app.mainwindow.tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
self._args = [cur_url if e == '{url}' else e for e in self._args]
|
||||
args = replace_variables(self._args)
|
||||
if count is not None:
|
||||
self._cmd.run(self._args, count=count)
|
||||
self._cmd.run(args, count=count)
|
||||
else:
|
||||
self._cmd.run(self._args)
|
||||
|
||||
self._cmd.run(args)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def run_safely(self, text, count=None):
|
||||
|
@ -584,11 +584,11 @@ DATA = collections.OrderedDict([
|
||||
('keybind', sect.ValueList(
|
||||
typ.KeyBindingName(), typ.KeyBinding(),
|
||||
('o', 'set-cmd-text ":open "'),
|
||||
('go', 'set-cmd-text :open {url}'),
|
||||
('go', 'set-cmd-text ":open {url}"'),
|
||||
('O', 'set-cmd-text ":open -t "'),
|
||||
('gO', 'set-cmd-text :open -t {url}'),
|
||||
('gO', 'set-cmd-text ":open -t {url}"'),
|
||||
('xo', 'set-cmd-text ":open -b "'),
|
||||
('xO', 'set-cmd-text :open -b {url}'),
|
||||
('xO', 'set-cmd-text ":open -b {url}"'),
|
||||
('ga', 'open -t about:blank'),
|
||||
('d', 'tab-close'),
|
||||
('co', 'tab-only'),
|
||||
@ -607,9 +607,9 @@ DATA = collections.OrderedDict([
|
||||
(';i', 'hint images'),
|
||||
(';I', 'hint images tab'),
|
||||
('.i', 'hint images tab-bg'),
|
||||
(';o', 'hint links fill :open {hint-url}'),
|
||||
(';O', 'hint links fill :open -t {hint-url}'),
|
||||
('.o', 'hint links fill :open -b {hint-url}'),
|
||||
(';o', 'hint links fill ":open {hint-url}"'),
|
||||
(';O', 'hint links fill ":open -t {hint-url}"'),
|
||||
('.o', 'hint links fill ":open -b {hint-url}"'),
|
||||
(';y', 'hint links yank'),
|
||||
(';Y', 'hint links yank-primary'),
|
||||
(';r', 'hint links rapid'),
|
||||
|
@ -137,13 +137,13 @@ class TestDebug(unittest.TestCase):
|
||||
def test_dbg_signal_eliding(self):
|
||||
"""Test eliding in dbg_signal()."""
|
||||
self.assertEqual(debug.dbg_signal(self.signal,
|
||||
[12345678901234567890123]),
|
||||
'fake(1234567890123456789\u2026)')
|
||||
['x' * 201]),
|
||||
"fake('{}\u2026)".format('x' * 198))
|
||||
|
||||
def test_dbg_signal_newline(self):
|
||||
"""Test dbg_signal() with a newline."""
|
||||
self.assertEqual(debug.dbg_signal(self.signal, ['foo\nbar']),
|
||||
'fake(foo bar)')
|
||||
r"fake('foo\nbar')")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import enum
|
||||
import shutil
|
||||
import unittest
|
||||
import os.path
|
||||
@ -511,5 +512,25 @@ class NormalizeTests(unittest.TestCase):
|
||||
self.assertEqual(utils.normalize_keystr(orig), repl)
|
||||
|
||||
|
||||
class IsEnumTests(unittest.TestCase):
|
||||
|
||||
"""Test is_enum."""
|
||||
|
||||
def test_enum(self):
|
||||
"""Test is_enum with an enum."""
|
||||
e = enum.Enum('Foo', 'bar, baz')
|
||||
self.assertTrue(utils.is_enum(e))
|
||||
|
||||
def test_class(self):
|
||||
"""Test is_enum with a non-enum class."""
|
||||
# pylint: disable=multiple-statements,missing-docstring
|
||||
class Test: pass
|
||||
self.assertFalse(utils.is_enum(Test))
|
||||
|
||||
def test_object(self):
|
||||
"""Test is_enum with a non-enum object."""
|
||||
self.assertFalse(utils.is_enum(23))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -21,58 +21,11 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import QEvent, QCoreApplication
|
||||
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import config, style
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_crash(typ : ('exception', 'segfault') = 'exception'):
|
||||
"""Crash for debugging purposes.
|
||||
|
||||
Args:
|
||||
typ: either 'exception' or 'segfault'.
|
||||
|
||||
Raises:
|
||||
raises Exception when typ is not segfault.
|
||||
segfaults when typ is (you don't say...)
|
||||
"""
|
||||
if typ == 'segfault':
|
||||
# From python's Lib/test/crashers/bogus_code_obj.py
|
||||
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
|
||||
'', '', 1, b'')
|
||||
exec(co) # pylint: disable=exec-used
|
||||
raise Exception("Segfault failed (wat.)")
|
||||
else:
|
||||
raise Exception("Forced crash")
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_all_widgets():
|
||||
"""Print a list of all widgets to debug log."""
|
||||
s = QCoreApplication.instance().get_all_widgets()
|
||||
log.misc.debug(s)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_all_objects():
|
||||
"""Print a list of all objects to the debug log."""
|
||||
s = QCoreApplication.instance().get_all_objects()
|
||||
log.misc.debug(s)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_cache_stats():
|
||||
"""Print LRU cache stats."""
|
||||
config_info = config.instance().get.cache_info()
|
||||
style_info = style.get_stylesheet.cache_info()
|
||||
log.misc.debug('config: {}'.format(config_info))
|
||||
log.misc.debug('style: {}'.format(style_info))
|
||||
|
||||
|
||||
def log_events(klass):
|
||||
@ -211,12 +164,12 @@ def signal_name(sig):
|
||||
def _format_args(args=None, kwargs=None):
|
||||
"""Format a list of arguments/kwargs to a function-call like string."""
|
||||
if args is not None:
|
||||
arglist = [utils.compact_text(repr(arg), 50) for arg in args]
|
||||
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
|
||||
else:
|
||||
arglist = []
|
||||
if kwargs is not None:
|
||||
for k, v in kwargs.items():
|
||||
arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 50)))
|
||||
arglist.append('{}={}'.format(k, utils.compact_text(repr(v), 200)))
|
||||
return ', '.join(arglist)
|
||||
|
||||
|
||||
|
@ -19,10 +19,11 @@
|
||||
|
||||
"""Misc. utility commands exposed to the user."""
|
||||
|
||||
import types
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtRemoveInputHook, QCoreApplication
|
||||
|
||||
from functools import partial
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from qutebrowser.utils import usertypes, log
|
||||
from qutebrowser.commands import runners, cmdexc, cmdutils
|
||||
@ -58,29 +59,12 @@ def later(ms : int, *command : {'nargs': '+'}):
|
||||
"int representation.")
|
||||
_timers.append(timer)
|
||||
cmdline = ' '.join(command)
|
||||
timer.timeout.connect(partial(_commandrunner.run_safely, cmdline))
|
||||
timer.timeout.connect(functools.partial(
|
||||
_commandrunner.run_safely, cmdline))
|
||||
timer.timeout.connect(lambda: _timers.remove(timer))
|
||||
timer.start()
|
||||
|
||||
|
||||
@cmdutils.register(debug=True, name='debug-set-trace')
|
||||
def set_trace():
|
||||
"""Break into the debugger in the shell.
|
||||
|
||||
//
|
||||
|
||||
Based on http://stackoverflow.com/a/1745965/2085149
|
||||
"""
|
||||
if sys.stdout is not None:
|
||||
sys.stdout.flush()
|
||||
print()
|
||||
print("When done debugging, remember to execute:")
|
||||
print(" from PyQt5 import QtCore; QtCore.pyqtRestoreInputHook()")
|
||||
print("before executing c(ontinue).")
|
||||
pyqtRemoveInputHook()
|
||||
pdb.set_trace()
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_crash(typ: ('exception', 'segfault')='exception'):
|
||||
"""Crash for debugging purposes.
|
||||
|
@ -22,6 +22,7 @@
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import enum
|
||||
import shlex
|
||||
import os.path
|
||||
import urllib.request
|
||||
@ -494,3 +495,11 @@ def disabled_excepthook():
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
sys.excepthook = old_excepthook
|
||||
|
||||
|
||||
def is_enum(obj):
|
||||
"""Check if a given object is an enum."""
|
||||
try:
|
||||
return issubclass(obj, enum.Enum)
|
||||
except TypeError:
|
||||
return False
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
"""The commandline in the statusbar."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication, QUrl
|
||||
from PyQt5.QtWidgets import QSizePolicy, QApplication
|
||||
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
@ -161,7 +161,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
self.show_cmd.emit()
|
||||
|
||||
@cmdutils.register(instance='mainwindow.status.cmd', name='set-cmd-text')
|
||||
def set_cmd_text_command(self, *strings):
|
||||
def set_cmd_text_command(self, text):
|
||||
"""Preset the statusbar to some text.
|
||||
|
||||
//
|
||||
@ -170,9 +170,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
strings which will get joined.
|
||||
|
||||
Args:
|
||||
strings: A list of strings to set.
|
||||
text: The commandline to set.
|
||||
"""
|
||||
text = ' '.join(strings)
|
||||
app = QCoreApplication.instance()
|
||||
url = app.mainwindow.tabs.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
# FIXME we currently replace the URL in any place in the arguments,
|
||||
# rather than just replacing it if it is a dedicated argument. We could
|
||||
# split the args, but then trailing spaces would be lost, so I'm not
|
||||
# sure what's the best thing to do here
|
||||
text = text.replace('{url}', url)
|
||||
if not text[0] in modeparsers.STARTCHARS:
|
||||
raise cmdexc.CommandError(
|
||||
"Invalid command text '{}'.".format(text))
|
||||
|
@ -22,7 +22,7 @@
|
||||
import functools
|
||||
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
@ -205,7 +205,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Raise:
|
||||
CommandError if the current URL is invalid.
|
||||
"""
|
||||
url = self.currentWidget().cur_url
|
||||
widget = self.currentWidget()
|
||||
if widget is None:
|
||||
url = QUrl()
|
||||
else:
|
||||
url = widget.cur_url
|
||||
try:
|
||||
qtutils.ensure_valid(url)
|
||||
except qtutils.QtValueError as e:
|
||||
|
@ -221,20 +221,20 @@ class WebView(QWebView):
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if self._force_open_target is not None:
|
||||
self._open_target = self._force_open_target
|
||||
self.open_target = self._force_open_target
|
||||
self._force_open_target = None
|
||||
log.mouse.debug("Setting force target: {}".format(
|
||||
self._open_target))
|
||||
self.open_target))
|
||||
elif (e.button() == Qt.MidButton or
|
||||
e.modifiers() & Qt.ControlModifier):
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
self._open_target = usertypes.ClickTarget.tab_bg
|
||||
self.open_target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
self._open_target = usertypes.ClickTarget.tab
|
||||
self.open_target = usertypes.ClickTarget.tab
|
||||
log.mouse.debug("Middle click, setting target: {}".format(
|
||||
self._open_target))
|
||||
self.open_target))
|
||||
else:
|
||||
self._open_target = usertypes.ClickTarget.normal
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
def shutdown(self):
|
||||
|
Loading…
Reference in New Issue
Block a user