summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2019-09-14 20:42:03 +0200
committerFlorian Bruhin <me@the-compiler.org>2019-09-14 20:42:03 +0200
commitb1a8feb6ba2346642aef78b4440550912cd1c63b (patch)
tree25eb16a8263ad0108b8953c2301dd3772900c053
parente5e2195b3d7289f469cb407df40a0ffef60f7cbf (diff)
parentd3e3991c94d192f66c902dd824c89f73cd8fe1d8 (diff)
downloadqutebrowser-b1a8feb6ba2346642aef78b4440550912cd1c63b.tar.gz
qutebrowser-b1a8feb6ba2346642aef78b4440550912cd1c63b.zip
Merge branch 'scroll-performance'
-rw-r--r--doc/changelog.asciidoc9
-rw-r--r--qutebrowser/app.py3
-rw-r--r--qutebrowser/browser/browsertab.py3
-rw-r--r--qutebrowser/browser/history.py2
-rw-r--r--qutebrowser/browser/signalfilter.py10
-rw-r--r--qutebrowser/browser/webengine/interceptor.py7
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py8
-rw-r--r--qutebrowser/browser/webengine/webview.py6
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py4
-rw-r--r--qutebrowser/commands/runners.py2
-rw-r--r--qutebrowser/config/configinit.py1
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py2
-rw-r--r--qutebrowser/mainwindow/statusbar/percentage.py28
-rw-r--r--qutebrowser/mainwindow/statusbar/textbase.py3
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py2
-rw-r--r--qutebrowser/misc/crashsignal.py16
-rw-r--r--qutebrowser/misc/objects.py1
-rw-r--r--qutebrowser/misc/throttle.py102
-rw-r--r--qutebrowser/misc/utilcmds.py6
-rw-r--r--qutebrowser/qutebrowser.py3
-rw-r--r--scripts/dev/check_coverage.py2
-rwxr-xr-xscripts/dev/run_vulture.py1
-rw-r--r--tests/end2end/features/test_history_bdd.py4
-rw-r--r--tests/end2end/features/test_marks_bdd.py4
-rw-r--r--tests/end2end/features/test_scroll_bdd.py4
-rw-r--r--tests/end2end/features/test_sessions_bdd.py6
-rw-r--r--tests/end2end/features/test_utilcmds_bdd.py6
-rw-r--r--tests/end2end/fixtures/quteprocess.py7
-rw-r--r--tests/unit/browser/test_history.py7
-rw-r--r--tests/unit/browser/test_qutescheme.py1
-rw-r--r--tests/unit/completion/test_models.py6
-rw-r--r--tests/unit/mainwindow/statusbar/test_percentage.py5
-rw-r--r--tests/unit/misc/test_throttle.py157
33 files changed, 354 insertions, 74 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index c5099e28c..22df0e1e5 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -33,10 +33,11 @@ Changed
* qute-pass: Don't run `pass` if only a username is requested.
* qute-pass: Support private domains like `myrouter.local`.
* readability: Improved CSS styling
-- Various performance improvements:
- * When loading config files
- * When typing without any completion matches
- * For keyboard handling in general
+- Performance improvements in various areas:
+ * Loading config files
+ * Typing without any completion matches
+ * General keyboard handling
+ * Scrolling
- `:version` now shows details about the loaded autoconfig.yml/config.py.
- Hosts are now additionally looked up including their ports in netrc files.
- With Qt 5.10 or newer, qutebrowser now doesn't force software rendering with
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index eb468d6d1..cf35f918f 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -829,7 +829,6 @@ class Application(QApplication):
log.init.debug("Initializing application...")
- self._args = args
objreg.register('args', args)
objreg.register('app', self)
@@ -864,7 +863,7 @@ class Application(QApplication):
def exit(self, status):
"""Extend QApplication::exit to log the event."""
log.destroy.debug("Now calling QApplication::exit.")
- if 'debug-exit' in self._args.debug_flags:
+ if 'debug-exit' in objects.debug_flags:
if hunter is None:
print("Not logging late shutdown because hunter could not be "
"imported!", file=sys.stderr)
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 4bd76a16a..10f430e34 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -528,7 +528,8 @@ class AbstractScroller(QObject):
super().__init__(parent)
self._tab = tab
self._widget = None # type: typing.Optional[QWidget]
- self.perc_changed.connect(self._log_scroll_pos_change)
+ if 'log-scroll-pos' in objects.debug_flags:
+ self.perc_changed.connect(self._log_scroll_pos_change)
@pyqtSlot()
def _log_scroll_pos_change(self) -> None:
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index d49000d29..3cdce4853 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -328,7 +328,7 @@ class WebHistory(sql.SqlTable):
log.misc.warning("Ignoring invalid URL being added to history")
return
- if 'no-sql-history' in objreg.get('args').debug_flags:
+ if 'no-sql-history' in objects.debug_flags:
return
atime = int(atime) if (atime is not None) else int(time.time())
diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py
index 356cd27a6..7ea9f8628 100644
--- a/qutebrowser/browser/signalfilter.py
+++ b/qutebrowser/browser/signalfilter.py
@@ -40,7 +40,7 @@ class SignalFilter(QObject):
BLACKLIST: List of signal names which should not be logged.
"""
- BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress', 'cur_link_hovered']
+ BLACKLIST = {'cur_scroll_perc_changed', 'cur_progress', 'cur_link_hovered'}
def __init__(self, win_id, parent=None):
super().__init__(parent)
@@ -56,9 +56,12 @@ class SignalFilter(QObject):
Return:
A partial function calling _filter_signals with a signal.
"""
- return functools.partial(self._filter_signals, signal, tab)
+ log_signal = debug.signal_name(signal) not in self.BLACKLIST
+ return functools.partial(
+ self._filter_signals, signal=signal,
+ log_signal=log_signal, tab=tab)
- def _filter_signals(self, signal, tab, *args):
+ def _filter_signals(self, *args, signal, log_signal, tab):
"""Filter signals and trigger TabbedBrowser signals if needed.
Triggers signal if the original signal was sent from the _current_ tab
@@ -72,7 +75,6 @@ class SignalFilter(QObject):
tab: The WebView which the filter belongs to.
*args: The args to pass to the signal.
"""
- log_signal = debug.signal_name(signal) not in self.BLACKLIST
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
try:
diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py
index 0729188b7..47edabda8 100644
--- a/qutebrowser/browser/webengine/interceptor.py
+++ b/qutebrowser/browser/webengine/interceptor.py
@@ -29,6 +29,7 @@ from qutebrowser.config import config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug, qtutils
from qutebrowser.extensions import interceptors
+from qutebrowser.misc import objects
@attr.s
@@ -106,10 +107,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
interceptors.ResourceType.unknown,
}
- def __init__(self, args, parent=None):
- super().__init__(parent)
- self._args = args
-
def install(self, profile):
"""Install the interceptor on the given QWebEngineProfile."""
try:
@@ -136,7 +133,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
Args:
info: QWebEngineUrlRequestInfo &info
"""
- if 'log-requests' in self._args.debug_flags:
+ if 'log-requests' in objects.debug_flags:
resource_type = debug.qenum_key(QWebEngineUrlRequestInfo,
info.resourceType())
navigation_type = debug.qenum_key(QWebEngineUrlRequestInfo,
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index cee16db9f..fc11b75a5 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -36,7 +36,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
cookies, webenginedownloads,
webenginesettings, certificateerror)
-from qutebrowser.misc import miscwidgets
+from qutebrowser.misc import miscwidgets, objects
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
from qutebrowser.qt import sip
@@ -60,8 +60,7 @@ def init():
_qute_scheme_handler.install(webenginesettings.private_profile)
log.init.debug("Initializing request interceptor...")
- args = objreg.get('args')
- req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
+ req_interceptor = interceptor.RequestInterceptor(parent=app)
req_interceptor.install(webenginesettings.default_profile)
if webenginesettings.private_profile:
req_interceptor.install(webenginesettings.private_profile)
@@ -419,7 +418,6 @@ class WebEngineScroller(browsertab.AbstractScroller):
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
- self._args = objreg.get('args')
self._pos_perc = (0, 0)
self._pos_px = QPoint()
self._at_bottom = False
@@ -477,7 +475,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._at_bottom = math.ceil(pos.y()) >= scrollable_y
if (self._pos_perc != (perc_x, perc_y) or
- 'no-scroll-filtering' in self._args.debug_flags):
+ 'no-scroll-filtering' in objects.debug_flags):
self._pos_perc = perc_x, perc_y
self.perc_changed.emit(*self._pos_perc)
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index f71f11087..a9be81ab1 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -27,8 +27,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import webenginesettings, certificateerror
from qutebrowser.config import config
-from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
-from qutebrowser.misc import miscwidgets
+from qutebrowser.utils import log, debug, usertypes, qtutils
+from qutebrowser.misc import miscwidgets, objects
from qutebrowser.qt import sip
@@ -68,7 +68,7 @@ class WebEngineView(QWebEngineView):
This got introduced in Qt 5.11.0 and fixed in 5.12.0.
"""
- if 'lost-focusproxy' not in objreg.get('args').debug_flags:
+ if 'lost-focusproxy' not in objects.debug_flags:
proxy = self.focusProxy()
if proxy is not None:
return proxy
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index e4c8da66c..46abd5cf2 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -36,6 +36,7 @@ from qutebrowser.extensions import interceptors
from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
+from qutebrowser.misc import objects
if typing.TYPE_CHECKING:
# pylint: disable=unused-import,useless-suppression
@@ -154,7 +155,6 @@ class NetworkManager(QNetworkAccessManager):
super().__init__(parent)
log.init.debug("NetworkManager init done")
self.adopted_downloads = 0
- self._args = objreg.get('args')
self._win_id = win_id
self._tab_id = tab_id
self._private = private
@@ -412,7 +412,7 @@ class NetworkManager(QNetworkAccessManager):
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)
- if 'log-requests' in self._args.debug_flags:
+ if 'log-requests' in objects.debug_flags:
operation = debug.qenum_key(QNetworkAccessManager, op)
operation = operation.replace('Operation', '').upper()
log.webview.debug("{} {}, first-party {}".format(
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index b5f2ecd06..d8070a350 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -34,7 +34,7 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split, objects
if typing.TYPE_CHECKING:
- # pylint: disable=unused-import
+ # pylint: disable=unused-import,useless-suppression
from qutebrowser.mainwindow import tabbedbrowser
_ReplacementFunction = typing.Callable[['tabbedbrowser.TabbedBrowser'], str]
diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py
index 9dc15d6e9..74c07e3ab 100644
--- a/qutebrowser/config/configinit.py
+++ b/qutebrowser/config/configinit.py
@@ -80,6 +80,7 @@ def early_init(args: argparse.Namespace) -> None:
message.error("set: {} - {}".format(e.__class__.__name__, e))
objects.backend = get_backend(args)
+ objects.debug_flags = set(args.debug_flags)
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config.instance.changed.connect(_update_monospace_fonts)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 82e67a3e2..52da06572 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -232,7 +232,7 @@ class StatusBar(QWidget):
self.percentage.show()
elif segment == 'scroll_raw':
self._hbox.addWidget(self.percentage)
- self.percentage.raw = True
+ self.percentage.set_raw()
self.percentage.show()
elif segment == 'history':
self._hbox.addWidget(self.backforward)
diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py
index 90aea7910..5a6b66479 100644
--- a/qutebrowser/mainwindow/statusbar/percentage.py
+++ b/qutebrowser/mainwindow/statusbar/percentage.py
@@ -19,9 +19,10 @@
"""Scroll percentage displayed in the statusbar."""
-from PyQt5.QtCore import pyqtSlot
+from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.mainwindow.statusbar import textbase
+from qutebrowser.misc import throttle
class Percentage(textbase.TextBase):
@@ -30,9 +31,20 @@ class Percentage(textbase.TextBase):
def __init__(self, parent=None):
"""Constructor. Set percentage to 0%."""
- super().__init__(parent)
+ super().__init__(parent, elidemode=Qt.ElideNone)
+ self._strings = self._calc_strings()
+ self._set_text = throttle.Throttle(self.setText, 100, parent=self)
self.set_perc(0, 0)
- self.raw = False
+
+ def set_raw(self):
+ self._strings = self._calc_strings(raw=True)
+
+ def _calc_strings(self, raw=False):
+ """Pre-calculate strings for the statusbar."""
+ fmt = '[{:02}]' if raw else '[{:02}%]'
+ strings = {i: fmt.format(i) for i in range(1, 100)}
+ strings.update({0: '[top]', 100: '[bot]'})
+ return strings
@pyqtSlot(int, int)
def set_perc(self, x, y): # pylint: disable=unused-argument
@@ -42,15 +54,7 @@ class Percentage(textbase.TextBase):
x: The x percentage (int), currently ignored.
y: The y percentage (int)
"""
- if y == 0:
- self.setText('[top]')
- elif y == 100:
- self.setText('[bot]')
- elif y is None:
- self.setText('[???]')
- else:
- text = '[{:02}]' if self.raw else '[{:02}%]'
- self.setText(text.format(y))
+ self._set_text(self._strings.get(y, '[???]'))
def on_tab_changed(self, tab):
"""Update scroll position when tab changed."""
diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py
index 8cfe0a846..2ee36a940 100644
--- a/qutebrowser/mainwindow/statusbar/textbase.py
+++ b/qutebrowser/mainwindow/statusbar/textbase.py
@@ -68,7 +68,8 @@ class TextBase(QLabel):
txt: The text to set (string).
"""
super().setText(txt)
- self._update_elided_text(self.geometry().width())
+ if self._elidemode != Qt.ElideNone:
+ self._update_elided_text(self.geometry().width())
def resizeEvent(self, e):
"""Extend QLabel::resizeEvent to update the elided text afterwards."""
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 9f697f498..b72984229 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -205,7 +205,7 @@ class TabbedBrowser(QWidget):
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
- title_format = config.val.window.title_format
+ title_format = config.cache['window.title_format']
if field is not None and ('{' + field + '}') not in title_format:
return
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index 8dae7cfb5..286a4c339 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -38,7 +38,7 @@ from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
from qutebrowser.api import cmdutils
-from qutebrowser.misc import earlyinit, crashdialog, ipc
+from qutebrowser.misc import earlyinit, crashdialog, ipc, objects
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
@@ -157,9 +157,9 @@ class CrashHandler(QObject):
"""Report a bug in qutebrowser."""
pages = self._recover_pages()
cmd_history = objreg.get('command-history')[-5:]
- objects = debug.get_all_objects()
+ all_objects = debug.get_all_objects()
self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history,
- objects)
+ all_objects)
self._crash_dialog.show()
def destroy_crashlogfile(self):
@@ -198,11 +198,11 @@ class CrashHandler(QObject):
cmd_history = []
try:
- objects = debug.get_all_objects()
+ all_objects = debug.get_all_objects()
except Exception:
log.destroy.exception("Error while getting objects")
- objects = ""
- return ExceptionInfo(pages, cmd_history, objects)
+ all_objects = ""
+ return ExceptionInfo(pages, cmd_history, all_objects)
def exception_hook(self, exctype, excvalue, tb):
"""Handle uncaught python exceptions.
@@ -222,10 +222,10 @@ class CrashHandler(QObject):
is_ignored_exception = (exctype is bdb.BdbQuit or
not issubclass(exctype, Exception))
- if 'pdb-postmortem' in self._args.debug_flags:
+ if 'pdb-postmortem' in objects.debug_flags:
pdb.post_mortem(tb)
- if is_ignored_exception or 'pdb-postmortem' in self._args.debug_flags:
+ if is_ignored_exception or 'pdb-postmortem' in objects.debug_flags:
# pdb exit, KeyboardInterrupt, ...
sys.exit(usertypes.Exit.exception)
diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py
index b903d8e5d..e2885ca22 100644
--- a/qutebrowser/misc/objects.py
+++ b/qutebrowser/misc/objects.py
@@ -39,3 +39,4 @@ class NoBackend:
backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend]
commands = {} # type: typing.Dict[str, command.Command]
+debug_flags = set() # type: typing.Set[str]
diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py
new file mode 100644
index 000000000..9bc8c620c
--- /dev/null
+++ b/qutebrowser/misc/throttle.py
@@ -0,0 +1,102 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2018-2019 Jay Kamat <jaygkamat@gmail.com>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""A throttle for throttling function calls."""
+
+import typing
+import time
+
+import attr
+from PyQt5.QtCore import QObject
+
+from qutebrowser.utils import usertypes
+
+
+@attr.s
+class _CallArgs:
+
+ args = attr.ib() # type: typing.Sequence[typing.Any]
+ kwargs = attr.ib() # type: typing.Mapping[str, typing.Any]
+
+
+class Throttle(QObject):
+
+ """A throttle to throttle calls.
+
+ If a request comes in, it will be processed immediately. If another request
+ comes in too soon, it is ignored, but will be processed when a timeout
+ ends. If another request comes in, it will update the pending request.
+ """
+
+ def __init__(self,
+ func: typing.Callable,
+ delay_ms: int,
+ parent: QObject = None) -> None:
+ """Constructor.
+
+ Args:
+ delay_ms: The time to wait before allowing another call of the
+ function. -1 disables the wrapper.
+ func: The function/method to call on __call__.
+ parent: The parent object.
+ """
+ super().__init__(parent)
+ self._delay_ms = delay_ms
+ self._func = func
+ self._pending_call = None # type: typing.Optional[_CallArgs]
+ self._last_call_ms = None # type: typing.Optional[int]
+ self._timer = usertypes.Timer(self, 'throttle-timer')
+ self._timer.setSingleShot(True)
+
+ def _call_pending(self):
+ """Start a pending call."""
+ self._func(*self._pending_call.args, **self._pending_call.kwargs)
+ self._pending_call = None
+ self._last_call_ms = int(time.monotonic() * 1000)
+
+ def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
+ cur_time_ms = int(time.monotonic() * 1000)
+ if self._pending_call is None:
+ if (self._last_call_ms is None or
+ cur_time_ms - self._last_call_ms > self._delay_ms):
+ # Call right now
+ self._last_call_ms = cur_time_ms
+ self._func(*args, **kwargs)
+ return
+
+ self._timer.setInterval(self._delay_ms -
+ (cur_time_ms - self._last_call_ms))
+ # Disconnect any existing calls, continue if no connections.
+ try:
+ self._timer.timeout.disconnect()
+ except TypeError:
+ pass
+ self._timer.timeout.connect(self._call_pending)
+ self._timer.start()
+
+ # Update arguments for an existing pending call
+ self._pending_call = _CallArgs(args=args, kwargs=kwargs)
+
+ def set_delay(self, delay_ms: int) -> None:
+ """Set the delay to wait between invocation of this function."""
+ self._delay_ms = delay_ms
+
+ def cancel(self) -> None:
+ """Cancel any pending instance of this timer."""
+ self._timer.stop()
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index a76826d80..93cf20be8 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -19,19 +19,21 @@
"""Misc. utility commands exposed to the user."""
+# QApplication and objects are imported so they're usable in :debug-pyeval
+
import functools
import os
import traceback
from PyQt5.QtCore import QUrl
-# so it's available for :debug-pyeval
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
-from qutebrowser.misc import consolewidget, debugcachestats
+from qutebrowser.misc import ( # pylint: disable=unused-import
+ consolewidget, debugcachestats, objects)
from qutebrowser.utils.version import pastebin_version
from qutebrowser.qt import sip
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index b76b966e7..05fdd8878 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -167,12 +167,13 @@ def debug_flag_error(flag):
no-sql-history: Don't store history items.
no-scroll-filtering: Process all scrolling updates.
log-requests: Log all network requests.
+ log-scroll-pos: Log all scrolling changes.
stack: Enable Chromium stack logging.
chromium: Enable Chromium logging.
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'lost-focusproxy',
- 'stack', 'chromium']
+ 'log-scroll-pos', 'stack', 'chromium']
if flag in valid_flags:
return flag
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 9048e282c..afdb686da 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -124,6 +124,8 @@ PERFECT_FILES = [
'misc/pastebin.py'),
('tests/unit/misc/test_objects.py',
'misc/objects.py'),
+ ('tests/unit/misc/test_throttle.py',
+ 'misc/throttle.py'),
(None,
'mainwindow/statusbar/keystring.py'),
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index c320d4325..ab38bdf09 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -68,6 +68,7 @@ def whitelist_generator(): # noqa
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
yield 'scripts.utils.bg_colors'
yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT'
+ yield 'qutebrowser.misc.throttle.Throttle.set_delay'
# Qt attributes
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
diff --git a/tests/end2end/features/test_history_bdd.py b/tests/end2end/features/test_history_bdd.py
index 6b0265cd6..44f64991e 100644
--- a/tests/end2end/features/test_history_bdd.py
+++ b/tests/end2end/features/test_history_bdd.py
@@ -30,8 +30,8 @@ bdd.scenarios('history.feature')
@pytest.fixture(autouse=True)
def turn_on_sql_history(quteproc):
"""Make sure SQL writing is enabled for tests in this module."""
- quteproc.send_cmd(":debug-pyeval objreg.get('args')."
- "debug_flags.remove('no-sql-history')")
+ cmd = ":debug-pyeval objects.debug_flags.remove('no-sql-history')"
+ quteproc.send_cmd(cmd)
quteproc.wait_for_load_finished_url('qute://pyeval')
diff --git a/tests/end2end/features/test_marks_bdd.py b/tests/end2end/features/test_marks_bdd.py
index 7dd3c3d2b..7495bfd88 100644
--- a/tests/end2end/features/test_marks_bdd.py
+++ b/tests/end2end/features/test_marks_bdd.py
@@ -25,9 +25,7 @@ bdd.scenarios('marks.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
- """Make sure all scrolling changes are logged."""
- quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
- "debug_flags.append('no-scroll-filtering')")
+ quteproc.turn_on_scroll_logging(no_scroll_filtering=True)
@bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}"))
diff --git a/tests/end2end/features/test_scroll_bdd.py b/tests/end2end/features/test_scroll_bdd.py
index 2358aa8a6..b45362928 100644
--- a/tests/end2end/features/test_scroll_bdd.py
+++ b/tests/end2end/features/test_scroll_bdd.py
@@ -25,6 +25,4 @@ bdd.scenarios('scroll.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
- """Make sure all scrolling changes are logged."""
- quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
- "debug_flags.append('no-scroll-filtering')")
+ quteproc.turn_on_scroll_logging(no_scroll_filtering=True)
diff --git a/tests/end2end/features/test_sessions_bdd.py b/tests/end2end/features/test_sessions_bdd.py
index 0b5a32359..52e7c5c86 100644
--- a/tests/end2end/features/test_sessions_bdd.py
+++ b/tests/end2end/features/test_sessions_bdd.py
@@ -20,10 +20,16 @@
import os.path
import logging
+import pytest
import pytest_bdd as bdd
bdd.scenarios('sessions.feature')
+@pytest.fixture(autouse=True)
+def turn_on_scroll_logging(quteproc):
+ quteproc.turn_on_scroll_logging()
+
+
@bdd.when(bdd.parsers.parse('I have a "{name}" session file:\n{contents}'))
def create_session_file(quteproc, name, contents):
filename = os.path.join(quteproc.basedir, 'data', 'sessions',
diff --git a/tests/end2end/features/test_utilcmds_bdd.py b/tests/end2end/features/test_utilcmds_bdd.py
index 16be24d1b..69b7d7213 100644
--- a/tests/end2end/features/test_utilcmds_bdd.py
+++ b/tests/end2end/features/test_utilcmds_bdd.py
@@ -17,6 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+import pytest
import pytest_bdd as bdd
bdd.scenarios('utilcmds.feature')
+
+
+@pytest.fixture(autouse=True)
+def turn_on_scroll_logging(quteproc):
+ quteproc.turn_on_scroll_logging()
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index c6c6bbcb9..c0a00624a 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -872,6 +872,13 @@ class QuteProc(testprocess.Process):
msg += '\nsee stdout for details'
pytest.fail(msg)
+ def turn_on_scroll_logging(self, no_scroll_filtering=False):
+ """Make sure all scrolling changes are logged."""
+ cmd = ":debug-pyeval -q objects.debug_flags.add('{}')"
+ if no_scroll_filtering:
+ self.send_cmd(cmd.format('no-scroll-filtering'))
+ self.send_cmd(cmd.format('log-scroll-pos'))
+
class YamlLoader(yaml.SafeLoader):
diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py
index 297fd4c2d..87984f1a5 100644
--- a/tests/unit/browser/test_history.py
+++ b/tests/unit/browser/test_history.py
@@ -27,13 +27,12 @@ from PyQt5.QtCore import QUrl
from qutebrowser.browser import history
from qutebrowser.utils import objreg, urlutils, usertypes
from qutebrowser.api import cmdutils
-from qutebrowser.misc import sql
+from qutebrowser.misc import sql, objects
@pytest.fixture(autouse=True)
def prerequisites(config_stub, fake_save_manager, init_sql, fake_args):
"""Make sure everything is ready to initialize a WebHistory."""
- fake_args.debug_flags = []
config_stub.data = {'general': {'private-browsing': False}}
@@ -171,8 +170,8 @@ class TestAdd:
expected = [(completion_url, title, atime)]
assert list(web_history.completion) == expected
- def test_no_sql_web_history(self, web_history, fake_args):
- fake_args.debug_flags = 'no-sql-history'
+ def test_no_sql_web_history(self, web_history, monkeypatch):
+ monkeypatch.setattr(objects, 'debug_flags', {'no-sql-history'})
web_history.add_url(QUrl('https://www.example.com/'), atime=12346,
title='Hello World', redirect=False)
assert not list(web_history)
diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py
index 0be63fa62..932c394a4 100644
--- a/tests/unit/browser/test_qutescheme.py
+++ b/tests/unit/browser/test_qutescheme.py
@@ -101,7 +101,6 @@ class TestHistoryHandler:
@pytest.fixture(autouse=True)
def fake_history(self, web_history, fake_args, entries):
"""Create fake history."""
- fake_args.debug_flags = []
for item in entries:
web_history.add_url(**item)
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 24e53648e..3d3ca0bf3 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -430,12 +430,6 @@ def test_bookmark_completion_delete(qtmodeltester, bookmarks, row, removed):
assert before.difference(after) == {removed}
-@pytest.fixture(autouse=True)
-def url_args(fake_args):
- """Prepare arguments needed to test the URL completion."""
- fake_args.debug_flags = []
-
-
def test_url_completion(qtmodeltester, config_stub, web_history_populated,
quickmarks, bookmarks, info):
"""Test the results of url completion.
diff --git a/tests/unit/mainwindow/statusbar/test_percentage.py b/tests/unit/mainwindow/statusbar/test_percentage.py
index 1488ab827..45846bd16 100644
--- a/tests/unit/mainwindow/statusbar/test_percentage.py
+++ b/tests/unit/mainwindow/statusbar/test_percentage.py
@@ -29,6 +29,8 @@ from qutebrowser.mainwindow.statusbar.percentage import Percentage
def percentage(qtbot):
"""Fixture providing a Percentage widget."""
widget = Percentage()
+ # Force immediate update of percentage widget
+ widget._set_text.set_delay(-1)
qtbot.add_widget(widget)
return widget
@@ -55,7 +57,8 @@ def test_percentage_text(percentage, y, raw, expected):
parametrized.
expected: expected text given y position. parametrized.
"""
- percentage.raw = raw
+ if raw:
+ percentage.set_raw()
percentage.set_perc(x=None, y=y)
assert percentage.text() == expected
diff --git a/tests/unit/misc/test_throttle.py b/tests/unit/misc/test_throttle.py
new file mode 100644
index 000000000..6dbe3a1f8
--- /dev/null
+++ b/tests/unit/misc/test_throttle.py
@@ -0,0 +1,157 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2018-2019 Jay Kamat <jaygkamat@gmail.com>:
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""Tests for qutebrowser.misc.throttle."""
+
+from unittest import mock
+
+import sip
+import pytest
+from PyQt5.QtCore import QObject
+
+from qutebrowser.misc import throttle
+
+
+@pytest.fixture
+def func():
+ return mock.Mock(spec=[])
+
+
+@pytest.fixture
+def throttled(func):
+ return throttle.Throttle(func, 100)
+
+
+def test_immediate(throttled, func, qapp):
+ throttled("foo")
+ throttled("foo")
+ func.assert_called_once_with("foo")
+
+
+def test_immediate_kwargs(throttled, func, qapp):
+ throttled(foo="bar")
+ throttled(foo="bar")
+ func.assert_called_once_with(foo="bar")
+
+
+def test_delayed(throttled, func, qtbot):
+ throttled("foo")
+ throttled("foo")
+ throttled("foo")
+ throttled("bar")
+ func.assert_called_once_with("foo")
+ func.reset_mock()
+
+ qtbot.wait(200)
+
+ func.assert_called_once_with("bar")
+
+
+def test_delayed_immediate_delayed(throttled, func, qtbot):
+ throttled("foo")
+ throttled("foo")
+ throttled("foo")
+ throttled("bar")
+ func.assert_called_once_with("foo")
+ func.reset_mock()
+
+ qtbot.wait(400)
+
+ func.assert_called_once_with("bar")
+ func.reset_mock()
+ throttled("baz")
+ throttled("baz")
+ throttled("bop")
+
+ func.assert_called_once_with("baz")
+ func.reset_mock()
+
+ qtbot.wait(200)
+
+ func.assert_called_once_with("bop")
+
+
+def test_delayed_delayed(throttled, func, qtbot):
+ throttled("foo")
+ throttled("foo")
+ throttled("foo")
+ throttled("bar")
+ func.assert_called_once_with("foo")
+ func.reset_mock()
+
+ qtbot.wait(150)
+
+ func.assert_called_once_with("bar")
+ func.reset_mock()
+ throttled("baz")
+ throttled("baz")
+ throttled("bop")
+
+ qtbot.wait(200)
+
+ func.assert_called_once_with("bop")
+ func.reset_mock()
+
+
+def test_cancel(throttled, func, qtbot):
+ throttled("foo")
+ throttled("foo")
+ throttled("foo")
+ throttled("bar")
+ func.assert_called_once_with("foo")
+ func.reset_mock()
+ throttled.cancel()
+
+ qtbot.wait(150)
+
+ func.assert_not_called()
+ func.reset_mock()
+
+
+def test_set(func, qtbot):
+ throttled = throttle.Throttle(func, 100)
+ throttled.set_delay(100)
+ throttled("foo")
+ throttled("foo")
+ throttled("foo")
+ throttled("bar")
+ func.assert_called_once_with("foo")
+ func.reset_mock()
+
+ qtbot.wait(150)
+
+ func.assert_called_once_with("bar")
+ func.reset_mock()
+
+
+def test_deleted_object(qtbot):
+ class Obj(QObject):
+
+ def func(self):
+ self.setObjectName("test")
+
+ obj = Obj()
+
+ throttled = throttle.Throttle(obj.func, 100, parent=obj)
+ throttled()
+ throttled()
+
+ sip.delete(obj)
+
+ qtbot.wait(150)