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 # E222: Multiple spaces after operator
# F811: Redifiniton # F811: Redifiniton
# W292: No newline at end of file # W292: No newline at end of file
# E701: multiple statements on one line
# E702: 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 max_complexity = 12
exclude = ez_setup.py exclude = ez_setup.py

View File

@ -233,7 +233,7 @@ class CommandDispatcher:
else: else:
diag = QPrintDialog() diag = QPrintDialog()
diag.setAttribute(Qt.WA_DeleteOnClose) 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') @cmdutils.register(instance='mainwindow.tabs.cmd')
def back(self, count=1): def back(self, count=1):
@ -257,7 +257,7 @@ class CommandDispatcher:
@cmdutils.register(instance='mainwindow.tabs.cmd') @cmdutils.register(instance='mainwindow.tabs.cmd')
def hint(self, group=webelem.Group.all, target=hints.Target.normal, def hint(self, group=webelem.Group.all, target=hints.Target.normal,
*args : {'nargs': '*'}): args=None):
"""Start hinting. """Start hinting.
Args: Args:
@ -286,7 +286,7 @@ class CommandDispatcher:
link. link.
- `spawn`: Spawn a command. - `spawn`: Spawn a command.
*args: Arguments for spawn/userscript/fill. args: Arguments for spawn/userscript/fill.
- With `spawn`: The executable and arguments to spawn. - With `spawn`: The executable and arguments to spawn.
`{hint-url}` will get replaced by the selected `{hint-url}` will get replaced by the selected
@ -301,7 +301,7 @@ class CommandDispatcher:
if frame is None: if frame is None:
raise cmdexc.CommandError("No frame focused!") raise cmdexc.CommandError("No frame focused!")
widget.hintmanager.start(frame, self._tabs.current_url(), group, widget.hintmanager.start(frame, self._tabs.current_url(), group,
target, *args) target, args)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) @cmdutils.register(instance='mainwindow.tabs.cmd', hide=True)
def follow_hint(self): def follow_hint(self):
@ -364,7 +364,7 @@ class CommandDispatcher:
Qt.Horizontal if horizontal else Qt.Vertical) Qt.Horizontal if horizontal else Qt.Vertical)
@cmdutils.register(instance='mainwindow.tabs.cmd', hide=True) @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. """Scroll the frame page-wise.
Args: Args:
@ -402,7 +402,8 @@ class CommandDispatcher:
target = "clipboard" target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, s)) log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode) 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') @cmdutils.register(instance='mainwindow.tabs.cmd')
def zoom_in(self, count=1): def zoom_in(self, count=1):
@ -518,7 +519,7 @@ class CommandDispatcher:
widget.openurl(url) widget.openurl(url)
@cmdutils.register(instance='mainwindow.tabs.cmd') @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]. """Select the tab given as argument/[count].
Args: Args:

View File

@ -20,6 +20,7 @@
"""A HintManager to draw hints over links.""" """A HintManager to draw hints over links."""
import math import math
import shlex
import subprocess import subprocess
import collections import collections
@ -472,7 +473,7 @@ class HintManager(QObject):
self.openurl.emit(url, newtab) self.openurl.emit(url, newtab)
def start(self, mainframe, baseurl, group=webelem.Group.all, def start(self, mainframe, baseurl, group=webelem.Group.all,
target=Target.normal, *args): target=Target.normal, args=None):
"""Start hinting. """Start hinting.
Args: Args:
@ -480,7 +481,7 @@ class HintManager(QObject):
baseurl: URL of the current page. baseurl: URL of the current page.
group: Which group of elements to hint. group: Which group of elements to hint.
target: What to do with the link. See attribute docstring. target: What to do with the link. See attribute docstring.
*args: Arguments for userscript/download args: Arguments for userscript/download
Emit: Emit:
hint_strings_updated: Emitted to update keypraser. hint_strings_updated: Emitted to update keypraser.
@ -493,14 +494,13 @@ class HintManager(QObject):
# on_mode_left, we are extra careful here. # on_mode_left, we are extra careful here.
raise ValueError("start() was called with frame=None") raise ValueError("start() was called with frame=None")
if target in (Target.userscript, Target.spawn, Target.fill): if target in (Target.userscript, Target.spawn, Target.fill):
if not args: if args is None:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Additional arguments are required with target " "'args' is required with target userscript/spawn/fill.")
"userscript/spawn/fill.")
else: else:
if args: if args is not None:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Arguments are only allowed with target userscript/spawn.") "'args' is only allowed with target userscript/spawn.")
elems = [] elems = []
ctx = HintContext() ctx = HintContext()
ctx.frames = webelem.get_child_frames(mainframe) ctx.frames = webelem.get_child_frames(mainframe)
@ -514,7 +514,13 @@ class HintManager(QObject):
raise cmdexc.CommandError("No elements found.") raise cmdexc.CommandError("No elements found.")
ctx.target = target ctx.target = target
ctx.baseurl = baseurl 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]) message.instance().set_text(self.HINT_TEXTS[target])
strings = self._hint_strings(visible_elems) strings = self._hint_strings(visible_elems)
for e, string in zip(visible_elems, strings): for e, string in zip(visible_elems, strings):

View File

@ -19,8 +19,12 @@
"""argparse.ArgumentParser subclass to parse qutebrowser commands.""" """argparse.ArgumentParser subclass to parse qutebrowser commands."""
import argparse import argparse
from qutebrowser.commands import cmdexc
from qutebrowser.utils import utils
class ArgumentParserError(Exception): class ArgumentParserError(Exception):
@ -29,6 +33,8 @@ class ArgumentParserError(Exception):
class ArgumentParser(argparse.ArgumentParser): class ArgumentParser(argparse.ArgumentParser):
"""Subclass ArgumentParser to be more suitable for runtime parsing."""
def __init__(self): def __init__(self):
super().__init__(add_help=False) super().__init__(add_help=False)
@ -37,4 +43,46 @@ class ArgumentParser(argparse.ArgumentParser):
'Status: {}, message: {}'.format(status, msg)) 'Status: {}, message: {}'.format(status, msg))
def error(self, 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 pass
class ArgumentTypeError(CommandMetaError):
"""Raised when an argument had an invalid type."""
pass
class PrerequisitesError(CommandMetaError): class PrerequisitesError(CommandMetaError):
"""Raised when a cmd can't be used because some prerequisites aren't met. """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. cmd_dict: A mapping from command-strings to command objects.
""" """
import enum
import inspect import inspect
import collections 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 from qutebrowser.commands import command, cmdexc, argparser
cmd_dict = {} cmd_dict = {}
@ -160,15 +160,16 @@ class register: # pylint: disable=invalid-name
""" """
names = self._get_names(func) names = self._get_names(func)
log.commands.vdebug("Registering command {}".format(names[0])) 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)) 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( cmd = command.Command(
name=names[0], split=self.split, hide=self.hide, count=has_count, name=names[0], split=self.split, hide=self.hide, count=has_count,
desc=desc, instance=self.instance, handler=func, desc=desc, instance=self.instance, handler=func,
completion=self.completion, modes=self.modes, completion=self.completion, modes=self.modes,
not_modes=self.not_modes, needs_js=self.needs_js, debug=self.debug, not_modes=self.not_modes, needs_js=self.needs_js,
parser=parser) is_debug=self.debug, parser=parser, type_conv=type_conv)
for name in names: for name in names:
cmd_dict[name] = cmd cmd_dict[name] = cmd
return func return func
@ -202,15 +203,17 @@ class register: # pylint: disable=invalid-name
func: The function to look at. func: The function to look at.
Return: Return:
A (has_count, desc, parser) tuple. A (has_count, desc, parser, type_conv) tuple.
has_count: Whether the command supports a count. has_count: Whether the command supports a count.
desc: The description of the command. desc: The description of the command.
parser: The ArgumentParser to use when parsing the commandline. 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) signature = inspect.signature(func)
if 'self' in signature.parameters and self.instance is None: if 'self' in signature.parameters and self.instance is None:
raise ValueError("{} is a class method, but instance was not " raise ValueError("{} is a class method, but instance was not "
"given!".format(mainname)) "given!".format(self.name[0]))
has_count = 'count' in signature.parameters has_count = 'count' in signature.parameters
parser = argparser.ArgumentParser() parser = argparser.ArgumentParser()
if func.__doc__ is not None: if func.__doc__ is not None:
@ -224,43 +227,64 @@ class register: # pylint: disable=invalid-name
args = [] args = []
kwargs = {} kwargs = {}
annotation_info = self._parse_annotation(param) annotation_info = self._parse_annotation(param)
if annotation_info.typ is not None: kwargs.update(self._param_to_argparse_kw(
typ = annotation_info.typ param, annotation_info))
else:
typ = self._infer_type(param)
kwargs.update(self._type_to_argparse(typ))
kwargs.update(annotation_info.kwargs) kwargs.update(annotation_info.kwargs)
if (param.kind == inspect.Parameter.VAR_POSITIONAL and args += self._param_to_argparse_pos(param, annotation_info)
'nargs' not in kwargs): # annotation_info overrides it typ = self._get_type(param, annotation_info)
kwargs['nargs'] = '*' if utils.is_enum(typ):
is_flag = typ == bool type_conv[param.name] = argparser.enum_getter(typ)
args += self._get_argparse_args(param, annotation_info, elif isinstance(typ, tuple):
is_flag) type_conv[param.name] = argparser.multitype_conv(typ)
callsig = debug.format_call(parser.add_argument, args, callsig = debugutils.format_call(parser.add_argument, args,
kwargs, full=False) kwargs, full=False)
log.commands.vdebug('Adding arg {} of type {} -> {}'.format( log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
param.name, typ, callsig)) param.name, typ, callsig))
parser.add_argument(*args, **kwargs) 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. """Get a list of positional argparse arguments.
Args: Args:
param: The inspect.Parameter instance for the current parameter. param: The inspect.Parameter instance for the current parameter.
annotation_info: An AnnotationInfo tuple for the parameter. annotation_info: An AnnotationInfo tuple for the parameter.
is_flag: Whether the option is a flag or not.
""" """
args = [] args = []
name = annotation_info.name or param.name name = annotation_info.name or param.name
shortname = annotation_info.flag or param.name[0] 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(name))
args.append('-{}'.format(shortname)) args.append('-{}'.format(shortname))
else: else:
args.append(name) args.append(name)
return args 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): def _parse_annotation(self, param):
"""Get argparse arguments and type from a parameter annotation. """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. name: The long name if overridden.
""" """
info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None} info = {'kwargs': {}, 'typ': None, 'flag': None, 'name': None}
log.commands.vdebug("Parsing annotation {}".format(param.annotation))
if param.annotation is not inspect.Parameter.empty: if param.annotation is not inspect.Parameter.empty:
log.commands.vdebug("Parsing annotation {}".format(
param.annotation))
if isinstance(param.annotation, dict): if isinstance(param.annotation, dict):
for field in ('type', 'flag', 'name'): for field in ('type', 'flag', 'name'):
if field in param.annotation: if field in param.annotation:
@ -288,27 +313,16 @@ class register: # pylint: disable=invalid-name
info['typ'] = param.annotation info['typ'] = param.annotation
return self.AnnotationInfo(**info) return self.AnnotationInfo(**info)
def _infer_type(self, param): def _get_type(self, param, annotation_info):
"""Get the type of an argument from its default value.""" """Get the type of an argument from its default value or annotation.
if param.default is None or param.default is inspect.Parameter.empty:
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 return None
else: else:
return type(param.default) 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 needs_js: Whether the command needs javascript enabled
debug: Whether this is a debugging command (only shown with --debug). debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command. parser: The ArgumentParser to use to parse this command.
type_conv: A mapping of conversion functions for arguments.
""" """
# TODO: # TODO:
@ -50,7 +51,8 @@ class Command:
# this might be combined with help texts or so as well # this might be combined with help texts or so as well
def __init__(self, name, split, hide, count, desc, instance, handler, 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. # I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
self.name = name self.name = name
@ -64,8 +66,9 @@ class Command:
self.modes = modes self.modes = modes
self.not_modes = not_modes self.not_modes = not_modes
self.needs_js = needs_js self.needs_js = needs_js
self.debug = debug self.debug = is_debug
self.parser = parser self.parser = parser
self.type_conv = type_conv
def _check_prerequisites(self): def _check_prerequisites(self):
"""Check if the command is permitted to run currently. """Check if the command is permitted to run currently.
@ -114,7 +117,7 @@ class Command:
try: try:
namespace = self.parser.parse_args(args) namespace = self.parser.parse_args(args)
except argparser.ArgumentParserError as e: except argparser.ArgumentParserError as e:
message.error(str(e)) message.error('{}: {}'.format(self.name, e))
return return
for name, arg in vars(namespace).items(): for name, arg in vars(namespace).items():
@ -124,6 +127,12 @@ class Command:
# FIXME: This approach is rather naive, but for now it works. # FIXME: This approach is rather naive, but for now it works.
posargs += arg posargs += arg
else: 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 kwargs[name] = arg
if self.instance is not None: if self.instance is not None:
@ -140,4 +149,7 @@ class Command:
self._check_prerequisites() self._check_prerequisites()
log.commands.debug('Calling {}'.format( log.commands.debug('Calling {}'.format(
debug.format_call(self.handler, posargs, kwargs))) 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) self.handler(*posargs, **kwargs)

View File

@ -27,6 +27,20 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, utils 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): class SearchRunner(QObject):
"""Run searches on webpages. """Run searches on webpages.
@ -219,6 +233,14 @@ class CommandRunner:
else: else:
raise cmdexc.NoSuchCommandError( raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr)) '{}: 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: if argstr is None:
self._args = [] self._args = []
elif self._cmd.split: elif self._cmd.split:
@ -244,10 +266,6 @@ class CommandRunner:
# If there are only flags, we got it right on the first try # If there are only flags, we got it right on the first try
# already. # already.
self._args = split_args self._args = split_args
retargs = self._args[:]
if text.endswith(' '):
retargs.append('')
return [cmdstr] + retargs
def run(self, text, count=None): def run(self, text, count=None):
"""Parse a command from a line of text and run it. """Parse a command from a line of text and run it.
@ -261,15 +279,11 @@ class CommandRunner:
self.run(sub, count) self.run(sub, count)
return return
self.parse(text) self.parse(text)
app = QCoreApplication.instance() args = replace_variables(self._args)
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]
if count is not None: if count is not None:
self._cmd.run(self._args, count=count) self._cmd.run(args, count=count)
else: else:
self._cmd.run(self._args) self._cmd.run(args)
@pyqtSlot(str, int) @pyqtSlot(str, int)
def run_safely(self, text, count=None): def run_safely(self, text, count=None):

View File

@ -584,11 +584,11 @@ DATA = collections.OrderedDict([
('keybind', sect.ValueList( ('keybind', sect.ValueList(
typ.KeyBindingName(), typ.KeyBinding(), typ.KeyBindingName(), typ.KeyBinding(),
('o', 'set-cmd-text ":open "'), ('o', 'set-cmd-text ":open "'),
('go', 'set-cmd-text :open {url}'), ('go', 'set-cmd-text ":open {url}"'),
('O', 'set-cmd-text ":open -t "'), ('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 "'),
('xO', 'set-cmd-text :open -b {url}'), ('xO', 'set-cmd-text ":open -b {url}"'),
('ga', 'open -t about:blank'), ('ga', 'open -t about:blank'),
('d', 'tab-close'), ('d', 'tab-close'),
('co', 'tab-only'), ('co', 'tab-only'),
@ -607,9 +607,9 @@ DATA = collections.OrderedDict([
(';i', 'hint images'), (';i', 'hint images'),
(';I', 'hint images tab'), (';I', 'hint images tab'),
('.i', 'hint images tab-bg'), ('.i', 'hint images tab-bg'),
(';o', 'hint links fill :open {hint-url}'), (';o', 'hint links fill ":open {hint-url}"'),
(';O', 'hint links fill :open -t {hint-url}'), (';O', 'hint links fill ":open -t {hint-url}"'),
('.o', 'hint links fill :open -b {hint-url}'), ('.o', 'hint links fill ":open -b {hint-url}"'),
(';y', 'hint links yank'), (';y', 'hint links yank'),
(';Y', 'hint links yank-primary'), (';Y', 'hint links yank-primary'),
(';r', 'hint links rapid'), (';r', 'hint links rapid'),

View File

@ -137,13 +137,13 @@ class TestDebug(unittest.TestCase):
def test_dbg_signal_eliding(self): def test_dbg_signal_eliding(self):
"""Test eliding in dbg_signal().""" """Test eliding in dbg_signal()."""
self.assertEqual(debug.dbg_signal(self.signal, self.assertEqual(debug.dbg_signal(self.signal,
[12345678901234567890123]), ['x' * 201]),
'fake(1234567890123456789\u2026)') "fake('{}\u2026)".format('x' * 198))
def test_dbg_signal_newline(self): def test_dbg_signal_newline(self):
"""Test dbg_signal() with a newline.""" """Test dbg_signal() with a newline."""
self.assertEqual(debug.dbg_signal(self.signal, ['foo\nbar']), self.assertEqual(debug.dbg_signal(self.signal, ['foo\nbar']),
'fake(foo bar)') r"fake('foo\nbar')")
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -21,6 +21,7 @@
import os import os
import sys import sys
import enum
import shutil import shutil
import unittest import unittest
import os.path import os.path
@ -511,5 +512,25 @@ class NormalizeTests(unittest.TestCase):
self.assertEqual(utils.normalize_keystr(orig), repl) 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -21,58 +21,11 @@
import re import re
import sys import sys
import types
import functools import functools
from PyQt5.QtCore import QEvent, QCoreApplication from PyQt5.QtCore import QEvent, QCoreApplication
from qutebrowser.utils import log, utils 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): def log_events(klass):
@ -211,12 +164,12 @@ def signal_name(sig):
def _format_args(args=None, kwargs=None): def _format_args(args=None, kwargs=None):
"""Format a list of arguments/kwargs to a function-call like string.""" """Format a list of arguments/kwargs to a function-call like string."""
if args is not None: 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: else:
arglist = [] arglist = []
if kwargs is not None: if kwargs is not None:
for k, v in kwargs.items(): 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) return ', '.join(arglist)

View File

@ -19,10 +19,11 @@
"""Misc. utility commands exposed to the user.""" """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.utils import usertypes, log
from qutebrowser.commands import runners, cmdexc, cmdutils from qutebrowser.commands import runners, cmdexc, cmdutils
@ -58,29 +59,12 @@ def later(ms : int, *command : {'nargs': '+'}):
"int representation.") "int representation.")
_timers.append(timer) _timers.append(timer)
cmdline = ' '.join(command) 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.timeout.connect(lambda: _timers.remove(timer))
timer.start() 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) @cmdutils.register(debug=True)
def debug_crash(typ: ('exception', 'segfault')='exception'): def debug_crash(typ: ('exception', 'segfault')='exception'):
"""Crash for debugging purposes. """Crash for debugging purposes.

View File

@ -22,6 +22,7 @@
import os import os
import io import io
import sys import sys
import enum
import shlex import shlex
import os.path import os.path
import urllib.request import urllib.request
@ -494,3 +495,11 @@ def disabled_excepthook():
# unchanged. Otherwise, we reset it. # unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__: if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_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.""" """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 PyQt5.QtWidgets import QSizePolicy, QApplication
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
@ -161,7 +161,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.show_cmd.emit() self.show_cmd.emit()
@cmdutils.register(instance='mainwindow.status.cmd', name='set-cmd-text') @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. """Preset the statusbar to some text.
// //
@ -170,9 +170,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
strings which will get joined. strings which will get joined.
Args: 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: if not text[0] in modeparsers.STARTCHARS:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text)) "Invalid command text '{}'.".format(text))

View File

@ -22,7 +22,7 @@
import functools import functools
from PyQt5.QtWidgets import QSizePolicy 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.QtGui import QIcon
from PyQt5.QtWebKitWidgets import QWebPage from PyQt5.QtWebKitWidgets import QWebPage
@ -205,7 +205,11 @@ class TabbedBrowser(tabwidget.TabWidget):
Raise: Raise:
CommandError if the current URL is invalid. 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: try:
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
except qtutils.QtValueError as e: except qtutils.QtValueError as e:

View File

@ -221,20 +221,20 @@ class WebView(QWebView):
e: The QMouseEvent. e: The QMouseEvent.
""" """
if self._force_open_target is not None: 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 self._force_open_target = None
log.mouse.debug("Setting force target: {}".format( log.mouse.debug("Setting force target: {}".format(
self._open_target)) self.open_target))
elif (e.button() == Qt.MidButton or elif (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier): e.modifiers() & Qt.ControlModifier):
if config.get('tabs', 'background-tabs'): if config.get('tabs', 'background-tabs'):
self._open_target = usertypes.ClickTarget.tab_bg self.open_target = usertypes.ClickTarget.tab_bg
else: else:
self._open_target = usertypes.ClickTarget.tab self.open_target = usertypes.ClickTarget.tab
log.mouse.debug("Middle click, setting target: {}".format( log.mouse.debug("Middle click, setting target: {}".format(
self._open_target)) self.open_target))
else: else:
self._open_target = usertypes.ClickTarget.normal self.open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target") log.mouse.debug("Normal click, setting normal target")
def shutdown(self): def shutdown(self):