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:
Florian Bruhin 2014-09-03 10:47:27 +02:00
parent d836e26107
commit 57d51ad9bb
17 changed files with 258 additions and 177 deletions

View File

@ -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

View File

@ -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):
@ -333,7 +333,7 @@ class CommandDispatcher:
self._prevnext(prev=False, newtab=tab)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll(self, dx : float, dy : float, count=1):
def scroll(self, dx: float, dy: float, count=1):
"""Scroll the current tab by 'count * dx/dy'.
Args:
@ -348,8 +348,8 @@ class CommandDispatcher:
self._current_widget().page().currentFrame().scroll(dx, dy)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def scroll_perc(self, perc : float = None,
horizontal : {'flag': 'x'} = False, count=None):
def scroll_perc(self, perc: float=None,
horizontal: {'flag': 'x'}=False, count=None):
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
@ -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:
@ -542,7 +543,7 @@ class CommandDispatcher:
idx))
@cmdutils.register(instance='mainwindow.tabs.cmd')
def tab_move(self, direction : ('+', '-') = None, count=None):
def tab_move(self, direction: ('+', '-')=None, count=None):
"""Move the current tab.
Args:

View File

@ -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):

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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'),
@ -637,7 +637,7 @@ DATA = collections.OrderedDict([
('B', 'set-cmd-text ":quickmark-load -t "'),
('sf', 'save'),
('ss', 'set-cmd-text ":set "'),
('sl', 'set-cmd-text ":set -t"'),
('sl', 'set-cmd-text ":set -t "'),
('sk', 'set-cmd-text ":set keybind "'),
('-', 'zoom-out'),
('+', 'zoom-in'),

View File

@ -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__':

View File

@ -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()

View File

@ -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)

View File

@ -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
@ -40,7 +41,7 @@ def init():
@cmdutils.register()
def later(ms : int, *command : {'nargs': '+'}):
def later(ms: int, *command: {'nargs': '+'}):
"""Execute a command after some time.
Args:
@ -58,31 +59,14 @@ 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'):
def debug_crash(typ: ('exception', 'segfault')='exception'):
"""Crash for debugging purposes.
Args:

View File

@ -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

View File

@ -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))

View File

@ -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:

View File

@ -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):