From ac1b59a3113e339f16896e32d41a1e5d6178dade Mon Sep 17 00:00:00 2001 From: Christian Helbling Date: Thu, 18 Oct 2018 17:48:49 +0200 Subject: [PATCH 001/117] revert 559059d and cd20c32 to fix #4349 (Exiting video player exits fullscreen) --- qutebrowser/browser/commands.py | 13 +++++++++++-- qutebrowser/mainwindow/mainwindow.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index a3f301ae5..8ea3c08a5 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -35,7 +35,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman, keyutils from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, standarddir) + objreg, utils, standarddir, debug) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess from qutebrowser.completion.models import urlmodel, miscmodels @@ -2234,7 +2234,16 @@ class CommandDispatcher: return window = self._tabbed_browser.widget.window() - window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) + if window.isFullScreen(): + window.setWindowState( + window.state_before_fullscreen & ~Qt.WindowFullScreen) + else: + window.state_before_fullscreen = window.windowState() + window.setWindowState( + window.state_before_fullscreen | Qt.WindowFullScreen) + log.misc.debug('state before fullscreen: {}'.format( + debug.qflags_key(Qt, window.state_before_fullscreen))) + @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-mute') diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 3fdad13d1..249e6e81d 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config, configfiles from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, - jinja) + jinja, debug) from qutebrowser.mainwindow import messageview, prompt from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman @@ -136,6 +136,7 @@ class MainWindow(QWidget): Attributes: status: The StatusBar widget. tabbed_browser: The TabbedBrowser widget. + state_before_fullscreen: window state before activation of fullscreen. _downloadview: The DownloadView widget. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. @@ -237,6 +238,8 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) self._set_decoration(config.val.window.hide_decoration) + self.state_before_fullscreen = self.windowState() + def _init_geometry(self, geometry): """Initialize the window geometry or load it from disk.""" if geometry is not None: @@ -516,9 +519,13 @@ class MainWindow(QWidget): def _on_fullscreen_requested(self, on): if not config.val.content.windowed_fullscreen: if on: - self.setWindowState(self.windowState() | Qt.WindowFullScreen) + self.state_before_fullscreen = self.windowState() + self.setWindowState( + Qt.WindowFullScreen | self.state_before_fullscreen) elif self.isFullScreen(): - self.setWindowState(self.windowState() & ~Qt.WindowFullScreen) + self.setWindowState(self.state_before_fullscreen) + log.misc.debug('on: {}, state before fullscreen: {}'.format( + on, debug.qflags_key(Qt, self.state_before_fullscreen))) @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() From af0648dd597554307ac9a4cedb9b494c92b17a0a Mon Sep 17 00:00:00 2001 From: Christian Helbling Date: Fri, 19 Oct 2018 09:46:42 +0200 Subject: [PATCH 002/117] remove superfluous blank line --- qutebrowser/browser/commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8ea3c08a5..b685c2c64 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2244,7 +2244,6 @@ class CommandDispatcher: log.misc.debug('state before fullscreen: {}'.format( debug.qflags_key(Qt, window.state_before_fullscreen))) - @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-mute') @cmdutils.argument('count', count=True) From 95e874fd265c8cd077e54b38b8dcd36c1978158b Mon Sep 17 00:00:00 2001 From: Christian Helbling Date: Sat, 20 Oct 2018 11:29:04 +0200 Subject: [PATCH 003/117] simplify and clearly separate the toggling code from the code to remember the state before fullscreen --- qutebrowser/browser/commands.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index b685c2c64..2760f18b6 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2234,13 +2234,11 @@ class CommandDispatcher: return window = self._tabbed_browser.widget.window() - if window.isFullScreen(): - window.setWindowState( - window.state_before_fullscreen & ~Qt.WindowFullScreen) - else: + + if not window.isFullScreen(): window.state_before_fullscreen = window.windowState() - window.setWindowState( - window.state_before_fullscreen | Qt.WindowFullScreen) + window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) + log.misc.debug('state before fullscreen: {}'.format( debug.qflags_key(Qt, window.state_before_fullscreen))) From 4166e50764dd8bef6187b74cece0073efdc5353d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 28 Oct 2018 15:27:02 +0100 Subject: [PATCH 004/117] Add support for per-domain secret files in qute-pass This adds support for password stores where the domain is not the directory name, but the filename of a gpg file. This solves problems when using a password store folder structure like this reddit user does: https://www.reddit.com/r/qutebrowser/comments/7owzl2/cant_get_qutepass_working/ --- misc/userscripts/qute-pass | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass index ca9c4d4ca..bfc2cbeb9 100755 --- a/misc/userscripts/qute-pass +++ b/misc/userscripts/qute-pass @@ -97,13 +97,19 @@ def qute_command(command): def find_pass_candidates(domain, password_store_path): candidates = [] for path, directories, file_names in os.walk(password_store_path, followlinks=True): - if directories or domain not in path.split(os.path.sep): + secrets = fnmatch.filter(file_names, '*.gpg') + if not secrets: continue # Strip password store path prefix to get the relative pass path pass_path = path[len(password_store_path) + 1:] - secrets = fnmatch.filter(file_names, '*.gpg') - candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets) + split_path = pass_path.split(os.path.sep) + for secret in secrets: + secret_base = os.path.splitext(secret)[0] + if domain not in (split_path + [secret_base]): + continue + + candidates.append(os.path.join(pass_path, secret_base)) return candidates From 735e6123cde46691d2d3d42cda8d8ec4618d9760 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:34:59 +0100 Subject: [PATCH 005/117] Fix lint --- qutebrowser/config/configdata.py | 2 +- qutebrowser/javascript/caret.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index c93032387..61e35fd53 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -25,7 +25,7 @@ DATA: A dict of Option objects after init() has been called. """ import typing -from typing import Optional # pylint: disable=unused-import +from typing import Optional # pylint: disable=unused-import,useless-suppression import functools import attr diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 28ab3fab0..5e6640311 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -1,5 +1,5 @@ /* eslint-disable max-len, max-statements, complexity, -default-case, valid-jsdoc */ +default-case */ // Copyright 2014 The Chromium Authors. All rights reserved. // From 411c5171a24b7c83829d6456d6a3c89b31bd6693 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 09:38:23 +0100 Subject: [PATCH 006/117] Load components dynamically --- qutebrowser/app.py | 4 ++-- qutebrowser/extensions/__init__.py | 0 qutebrowser/extensions/loader.py | 35 ++++++++++++++++++++++++++++++ qutebrowser/utils/log.py | 3 ++- scripts/dev/run_vulture.py | 3 +++ scripts/dev/src2asciidoc.py | 2 ++ 6 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 qutebrowser/extensions/__init__.py create mode 100644 qutebrowser/extensions/loader.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6c948e10c..65c7395eb 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -68,6 +68,7 @@ from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit.network import networkmanager +from qutebrowser.extensions import loader from qutebrowser.keyinput import macros from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.misc import (readline, ipc, savemanager, sessions, @@ -77,8 +78,6 @@ from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, usertypes, standarddir, error, qtutils) # pylint: disable=unused-import # We import those to run the cmdutils.register decorators. -from qutebrowser.components import (scrollcommands, caretcommands, - zoomcommands, misccommands) from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds # pylint: enable=unused-import @@ -166,6 +165,7 @@ def init(args, crash_handler): qApp.setQuitOnLastWindowClosed(False) _init_icon() + loader.load_components() try: _init_modules(args, crash_handler) except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: diff --git a/qutebrowser/extensions/__init__.py b/qutebrowser/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py new file mode 100644 index 000000000..9b5aadd25 --- /dev/null +++ b/qutebrowser/extensions/loader.py @@ -0,0 +1,35 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +"""Loader for qutebrowser extensions.""" + +import pkgutil + +from qutebrowser import components +from qutebrowser.utils import log + + +def load_components() -> None: + """Load everything from qutebrowser.components.""" + for info in pkgutil.walk_packages(components.__path__): + if info.ispkg: + continue + log.extensions.debug("Importing {}".format(info.name)) + loader = info.module_finder.find_module(info.name) + loader.load_module(info.name) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index bbc025515..115c53352 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -137,6 +137,7 @@ prompt = logging.getLogger('prompt') network = logging.getLogger('network') sql = logging.getLogger('sql') greasemonkey = logging.getLogger('greasemonkey') +extensions = logging.getLogger('extensions') LOGGER_NAMES = [ 'statusbar', 'completion', 'init', 'url', @@ -146,7 +147,7 @@ LOGGER_NAMES = [ 'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'save', 'message', 'config', 'sessions', 'webelem', 'prompt', 'network', 'sql', - 'greasemonkey' + 'greasemonkey', 'extensions', ] diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index f3217694e..7874f6a79 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -30,6 +30,7 @@ import argparse import vulture import qutebrowser.app # pylint: disable=unused-import +from qutebrowser.extensions import loader from qutebrowser.misc import objects from qutebrowser.utils import utils from qutebrowser.browser.webkit import rfc6266 @@ -43,6 +44,8 @@ from qutebrowser.config import configtypes def whitelist_generator(): # noqa """Generator which yields lines to add to a vulture whitelist.""" + loader.load_components() + # qutebrowser commands for cmd in objects.commands.values(): yield utils.qualname(cmd.handler) diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index ba4e9b69c..f0536c045 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -35,6 +35,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, # We import qutebrowser.app so all @cmdutils-register decorators are run. import qutebrowser.app from qutebrowser import qutebrowser, commands +from qutebrowser.extensions import loader from qutebrowser.commands import argparser from qutebrowser.config import configdata, configtypes from qutebrowser.utils import docutils, usertypes @@ -549,6 +550,7 @@ def regenerate_cheatsheet(): def main(): """Regenerate all documentation.""" utils.change_cwd() + loader.load_components() print("Generating manpage...") regenerate_manpage('doc/qutebrowser.1.asciidoc') print("Generating settings help...") From fcb39c1d7f41e4067980f8eeb4276c209c5033ce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 10:06:07 +0100 Subject: [PATCH 007/117] Add types to extensions.loader --- mypy.ini | 4 ++++ qutebrowser/extensions/loader.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/mypy.ini b/mypy.ini index 4526e4e48..8fb8d89ae 100644 --- a/mypy.ini +++ b/mypy.ini @@ -73,3 +73,7 @@ disallow_incomplete_defs = True [mypy-qutebrowser.components.*] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.extensions.*] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 9b5aadd25..9674ad707 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -19,7 +19,9 @@ """Loader for qutebrowser extensions.""" +import importlib.abc import pkgutil +import types from qutebrowser import components from qutebrowser.utils import log @@ -27,9 +29,15 @@ from qutebrowser.utils import log def load_components() -> None: """Load everything from qutebrowser.components.""" - for info in pkgutil.walk_packages(components.__path__): - if info.ispkg: + for finder, name, ispkg in pkgutil.walk_packages(components.__path__): + if ispkg: continue - log.extensions.debug("Importing {}".format(info.name)) - loader = info.module_finder.find_module(info.name) - loader.load_module(info.name) + _load_module(finder, name) + + +def _load_module(finder: importlib.abc.PathEntryFinder, + name: str) -> types.ModuleType: + log.extensions.debug("Importing {}".format(name)) + loader = finder.find_module(name) + assert loader is not None + return loader.load_module(name) From 15e9127fa08c0348aae4fdcc3080f55110a16615 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 10:26:25 +0100 Subject: [PATCH 008/117] Add components to pyinstaller hiddenimports --- misc/qutebrowser.spec | 11 ++++++++++- qutebrowser/extensions/loader.py | 27 +++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index ff1b10577..b40172754 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -6,6 +6,8 @@ import os sys.path.insert(0, os.getcwd()) from scripts import setupcommon +from qutebrowser.extensions import loader + block_cipher = None @@ -27,6 +29,13 @@ def get_data_files(): return data_files +def get_hidden_imports(): + imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'] + for info in loader.walk_components(): + imports.append('qutebrowser.components.' + info.name) + return imports + + setupcommon.write_git_file() @@ -42,7 +51,7 @@ a = Analysis(['../qutebrowser/__main__.py'], pathex=['misc'], binaries=None, datas=get_data_files(), - hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'], + hiddenimports=get_hidden_imports(), hookspath=[], runtime_hooks=[], excludes=['tkinter'], diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 9674ad707..d6fdc675e 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -22,22 +22,37 @@ import importlib.abc import pkgutil import types +import typing + +import attr from qutebrowser import components from qutebrowser.utils import log +@attr.s +class ComponentInfo: + + name = attr.ib() # type: str + finder = attr.ib() # type: importlib.abc.PathEntryFinder + + def load_components() -> None: """Load everything from qutebrowser.components.""" + for info in walk_components(): + _load_component(info) + + +def walk_components() -> typing.Iterator[ComponentInfo]: + """Yield ComponentInfo objects for all modules.""" for finder, name, ispkg in pkgutil.walk_packages(components.__path__): if ispkg: continue - _load_module(finder, name) + yield ComponentInfo(name=name, finder=finder) -def _load_module(finder: importlib.abc.PathEntryFinder, - name: str) -> types.ModuleType: - log.extensions.debug("Importing {}".format(name)) - loader = finder.find_module(name) +def _load_component(info: ComponentInfo) -> types.ModuleType: + log.extensions.debug("Importing {}".format(info.name)) + loader = info.finder.find_module(info.name) assert loader is not None - return loader.load_module(name) + return loader.load_module(info.name) From 6bc771151fc7f6b67fb996697cec4443d467d0e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 11:19:37 +0100 Subject: [PATCH 009/117] Make walking components work for PyInstaller Closes https://github.com/qutebrowser/qutebrowser-extensions/issues/5 --- qutebrowser/extensions/loader.py | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index d6fdc675e..d7f9b2533 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -23,6 +23,7 @@ import importlib.abc import pkgutil import types import typing +import sys import attr @@ -34,7 +35,6 @@ from qutebrowser.utils import log class ComponentInfo: name = attr.ib() # type: str - finder = attr.ib() # type: importlib.abc.PathEntryFinder def load_components() -> None: @@ -45,14 +45,35 @@ def load_components() -> None: def walk_components() -> typing.Iterator[ComponentInfo]: """Yield ComponentInfo objects for all modules.""" - for finder, name, ispkg in pkgutil.walk_packages(components.__path__): + if hasattr(sys, 'frozen'): + yield from _walk_pyinstaller() + else: + yield from _walk_normal() + + +def _walk_normal() -> typing.Iterator[ComponentInfo]: + """Walk extensions when not using PyInstaller.""" + for _finder, name, ispkg in pkgutil.walk_packages(components.__path__): if ispkg: continue - yield ComponentInfo(name=name, finder=finder) + fullname = components.__name__ + '.' + name + yield ComponentInfo(name=fullname) + + +def _walk_pyinstaller() -> typing.Iterator[ComponentInfo]: + """Walk extensions when using PyInstaller. + + See https://github.com/pyinstaller/pyinstaller/issues/1905 + """ + toc = set() + for importer in pkgutil.iter_importers('qutebrowser'): + if hasattr(importer, 'toc'): + toc |= importer.toc + for name in toc: + if name.startswith(components.__name__ + '.'): + yield ComponentInfo(name=name) def _load_component(info: ComponentInfo) -> types.ModuleType: log.extensions.debug("Importing {}".format(info.name)) - loader = info.finder.find_module(info.name) - assert loader is not None - return loader.load_module(info.name) + return importlib.import_module(info.name) From bb115afc1a77fba01131a7fd447cd0a4471565f6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 11:41:37 +0100 Subject: [PATCH 010/117] Make sure we always yield full names --- misc/qutebrowser.spec | 2 +- qutebrowser/extensions/loader.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec index b40172754..269668751 100644 --- a/misc/qutebrowser.spec +++ b/misc/qutebrowser.spec @@ -32,7 +32,7 @@ def get_data_files(): def get_hidden_imports(): imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'] for info in loader.walk_components(): - imports.append('qutebrowser.components.' + info.name) + imports.append(info.name) return imports diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index d7f9b2533..2679e89dc 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -51,13 +51,19 @@ def walk_components() -> typing.Iterator[ComponentInfo]: yield from _walk_normal() +def _walk_error(name: str): + raise ImportError("Failed to import {}".format(name)) + + def _walk_normal() -> typing.Iterator[ComponentInfo]: """Walk extensions when not using PyInstaller.""" - for _finder, name, ispkg in pkgutil.walk_packages(components.__path__): + for _finder, name, ispkg in pkgutil.walk_packages( + path=components.__path__, + prefix=components.__name__ + '.', + onerror=_walk_error): if ispkg: continue - fullname = components.__name__ + '.' + name - yield ComponentInfo(name=fullname) + yield ComponentInfo(name=name) def _walk_pyinstaller() -> typing.Iterator[ComponentInfo]: From 38c4ef3623fd271aaa6708852af577d819cd1803 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 11:51:26 +0100 Subject: [PATCH 011/117] Fix lint --- qutebrowser/extensions/loader.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 2679e89dc..b2481b969 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -32,7 +32,9 @@ from qutebrowser.utils import log @attr.s -class ComponentInfo: +class ExtensionInfo: + + """Information about a qutebrowser extension.""" name = attr.ib() # type: str @@ -43,43 +45,48 @@ def load_components() -> None: _load_component(info) -def walk_components() -> typing.Iterator[ComponentInfo]: - """Yield ComponentInfo objects for all modules.""" +def walk_components() -> typing.Iterator[ExtensionInfo]: + """Yield ExtensionInfo objects for all modules.""" if hasattr(sys, 'frozen'): yield from _walk_pyinstaller() else: yield from _walk_normal() -def _walk_error(name: str): +def _on_walk_error(name: str) -> None: raise ImportError("Failed to import {}".format(name)) -def _walk_normal() -> typing.Iterator[ComponentInfo]: +def _walk_normal() -> typing.Iterator[ExtensionInfo]: """Walk extensions when not using PyInstaller.""" for _finder, name, ispkg in pkgutil.walk_packages( - path=components.__path__, + # Only packages have a __path__ attribute, + # but we're sure this is one. + path=components.__path__, # type: ignore prefix=components.__name__ + '.', - onerror=_walk_error): + onerror=_on_walk_error): if ispkg: continue - yield ComponentInfo(name=name) + yield ExtensionInfo(name=name) -def _walk_pyinstaller() -> typing.Iterator[ComponentInfo]: +def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: """Walk extensions when using PyInstaller. See https://github.com/pyinstaller/pyinstaller/issues/1905 + + Inspired by: + https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py """ - toc = set() + toc = set() # type: typing.Set[str] for importer in pkgutil.iter_importers('qutebrowser'): if hasattr(importer, 'toc'): toc |= importer.toc for name in toc: if name.startswith(components.__name__ + '.'): - yield ComponentInfo(name=name) + yield ExtensionInfo(name=name) -def _load_component(info: ComponentInfo) -> types.ModuleType: +def _load_component(info: ExtensionInfo) -> types.ModuleType: log.extensions.debug("Importing {}".format(info.name)) return importlib.import_module(info.name) From ec5a93a80dea06fed31f8779c837800658f8bb59 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:15:10 +0100 Subject: [PATCH 012/117] Add tests for extensions.loader --- tests/unit/extensions/test_loader.py | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unit/extensions/test_loader.py diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py new file mode 100644 index 000000000..e0c1912fa --- /dev/null +++ b/tests/unit/extensions/test_loader.py @@ -0,0 +1,49 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +import pytest + +from qutebrowser.extensions import loader +from qutebrowser.misc import objects + + +def test_on_walk_error(): + with pytest.raises(ImportError, match='Failed to import foo'): + loader._on_walk_error('foo') + + +def test_walk_normal(): + names = [info.name for info in loader._walk_normal()] + assert 'qutebrowser.components.scrollcommands' in names + + +def test_walk_pyinstaller(): + # We can't test whether we get something back without being frozen by + # PyInstaller, but at least we can test that we don't crash. + list(loader._walk_pyinstaller()) + + +def test_load_component(monkeypatch): + monkeypatch.setattr(objects, 'commands', {}) + + info = loader.ExtensionInfo(name='qutebrowser.components.scrollcommands') + module = loader._load_component(info) + + assert hasattr(module, 'scroll_to_perc') + assert 'scroll-to-perc' in objects.commands From 5b354164c5c4fbf6af1d58c185c7cd0510aedcbc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:30:47 +0100 Subject: [PATCH 013/117] Make it possible for extensions to define init hooks --- qutebrowser/api/apitypes.py | 1 + qutebrowser/api/hook.py | 35 +++++++++++++++++++++++++ qutebrowser/components/__init__.py | 2 +- qutebrowser/components/caretcommands.py | 2 +- qutebrowser/extensions/loader.py | 35 ++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 qutebrowser/api/hook.py diff --git a/qutebrowser/api/apitypes.py b/qutebrowser/api/apitypes.py index 9fec0a6cb..8fbc1a9a7 100644 --- a/qutebrowser/api/apitypes.py +++ b/qutebrowser/api/apitypes.py @@ -24,3 +24,4 @@ from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab from qutebrowser.browser.webelem import (Error as WebElemError, AbstractWebElement as WebElement) from qutebrowser.utils.usertypes import ClickTarget, JsWorld +from qutebrowser.extensions.loader import InitContext diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py new file mode 100644 index 000000000..b468e91f5 --- /dev/null +++ b/qutebrowser/api/hook.py @@ -0,0 +1,35 @@ +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +"""Hooks for extensions.""" + +import importlib +import types +import typing + + +from qutebrowser.extensions import loader + + +class init: # noqa: N801,N806 pylint: disable=invalid-name + + """Decorator to mark a function to run when initializing.""" + + def __call__(self, func: typing.Callable) -> typing.Callable: + module = importlib.import_module(func.__module__) + info = loader.add_module_info(module) + info.init_hook = func diff --git a/qutebrowser/components/__init__.py b/qutebrowser/components/__init__.py index b42c87fb6..1a13763bf 100644 --- a/qutebrowser/components/__init__.py +++ b/qutebrowser/components/__init__.py @@ -17,4 +17,4 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""qutebrowser "extensions" which only use the qutebrowser.API API.""" +"""qutebrowser "extensions" which only use the qutebrowser.api API.""" diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index 4bab6b6c6..b9ecfab7c 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -20,7 +20,7 @@ """Commands related to caret browsing.""" -from qutebrowser.api import cmdutils, apitypes +from qutebrowser.api import cmdutils, apitypes, hook @cmdutils.register(modes=[cmdutils.KeyMode.caret]) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index b2481b969..f8cafa9e3 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -31,6 +31,23 @@ from qutebrowser import components from qutebrowser.utils import log +@attr.s +class InitContext: + + """Context an extension gets in its init hook.""" + + +@attr.s +class ModuleInfo: + + """Information attached to an extension module. + + This gets used by qutebrowser.api.hook. + """ + + init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] + + @attr.s class ExtensionInfo: @@ -39,6 +56,13 @@ class ExtensionInfo: name = attr.ib() # type: str +def add_module_info(module: types.ModuleType) -> ModuleInfo: + """Add ModuleInfo to a module (if not added yet).""" + if not hasattr(module, '__qute_module_info'): + module.__qute_module_info = ModuleInfo() + return module.__qute_module_info + + def load_components() -> None: """Load everything from qutebrowser.components.""" for info in walk_components(): @@ -88,5 +112,14 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: def _load_component(info: ExtensionInfo) -> types.ModuleType: + """Load the given extension and run its init hook (if any).""" log.extensions.debug("Importing {}".format(info.name)) - return importlib.import_module(info.name) + mod = importlib.import_module(info.name) + + info = add_module_info(mod) + if info.init_hook is not None: + log.extensions.debug("Running init hook {!r}" + .format(info.init_hook.__name__)) + info.init_hook(InitContext()) + + return mod From 380905854cca120d0755c4824fd3c0d2b6f8b6fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:32:09 +0100 Subject: [PATCH 014/117] Pass standarddir to modules --- qutebrowser/extensions/loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index f8cafa9e3..38456dcf1 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -28,7 +28,7 @@ import sys import attr from qutebrowser import components -from qutebrowser.utils import log +from qutebrowser.utils import log, standarddir @attr.s @@ -36,6 +36,8 @@ class InitContext: """Context an extension gets in its init hook.""" + data_dir = attr.ib() # type: str + @attr.s class ModuleInfo: @@ -120,6 +122,7 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: if info.init_hook is not None: log.extensions.debug("Running init hook {!r}" .format(info.init_hook.__name__)) - info.init_hook(InitContext()) + context = InitContext(data_dir=standarddir.data()) + info.init_hook(context) return mod From b1894f142869d6588b848cc2bf7dae8596b34b87 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:42:00 +0100 Subject: [PATCH 015/117] Fix lint --- qutebrowser/api/hook.py | 4 +++- qutebrowser/components/caretcommands.py | 2 +- qutebrowser/extensions/loader.py | 13 +++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index b468e91f5..e7e0de88d 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -1,3 +1,5 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + # Copyright 2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. @@ -18,7 +20,6 @@ """Hooks for extensions.""" import importlib -import types import typing @@ -33,3 +34,4 @@ class init: # noqa: N801,N806 pylint: disable=invalid-name module = importlib.import_module(func.__module__) info = loader.add_module_info(module) info.init_hook = func + return func diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index b9ecfab7c..4bab6b6c6 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -20,7 +20,7 @@ """Commands related to caret browsing.""" -from qutebrowser.api import cmdutils, apitypes, hook +from qutebrowser.api import cmdutils, apitypes @cmdutils.register(modes=[cmdutils.KeyMode.caret]) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 38456dcf1..4f5e345c6 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -60,9 +60,10 @@ class ExtensionInfo: def add_module_info(module: types.ModuleType) -> ModuleInfo: """Add ModuleInfo to a module (if not added yet).""" + # pylint: disable=protected-access if not hasattr(module, '__qute_module_info'): - module.__qute_module_info = ModuleInfo() - return module.__qute_module_info + module.__qute_module_info = ModuleInfo() # type: ignore + return module.__qute_module_info # type: ignore def load_components() -> None: @@ -118,11 +119,11 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: log.extensions.debug("Importing {}".format(info.name)) mod = importlib.import_module(info.name) - info = add_module_info(mod) - if info.init_hook is not None: + mod_info = add_module_info(mod) + if mod_info.init_hook is not None: log.extensions.debug("Running init hook {!r}" - .format(info.init_hook.__name__)) + .format(mod_info.init_hook.__name__)) context = InitContext(data_dir=standarddir.data()) - info.init_hook(context) + mod_info.init_hook(context) return mod From 4ad579031160974952f66839f8b05d4018a8deb1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:08:32 +0100 Subject: [PATCH 016/117] Add API for temporary downloads Closes https://github.com/qutebrowser/qutebrowser-extensions/issues/9 --- qutebrowser/api/downloads.py | 56 ++++++++++++++++++++++++++++++++++ qutebrowser/browser/adblock.py | 9 ++---- 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 qutebrowser/api/downloads.py diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py new file mode 100644 index 000000000..f743f5d7a --- /dev/null +++ b/qutebrowser/api/downloads.py @@ -0,0 +1,56 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + + +import io + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl + +from qutebrowser.browser import downloads, qtnetworkdownloads +from qutebrowser.utils import objreg + + +class TempDownload(QObject): + + """A download of some data into a file object.""" + + finished = pyqtSignal() + + def __init__(self, item: qtnetworkdownloads.DownloadItem) -> None: + self._item = item + self._item.finished.connect(self._on_download_finished) + self.successful = False + self.fileobj = item.fileobj + + @pyqtSlot() + def _on_download_finished(self) -> None: + self.successful = self._item.successful + self.finished.emit() + + +def download_temp(url: QUrl) -> TempDownload: + """Download the given URL into a file object. + + The download is not saved to disk. + """ + fobj = io.BytesIO() + fobj.name = 'temporary: ' + url.host() + target = downloads.FileObjDownloadTarget(fobj) + download_manager = objreg.get('qtnetwork-download-manager') + return download_manager.get(url, target=target, auto_remove=True) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index fdec79d0f..917942b7f 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -25,7 +25,7 @@ import functools import posixpath import zipfile -from qutebrowser.browser import downloads +from qutebrowser.api import downloads from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir, log, message from qutebrowser.api import cmdutils @@ -173,7 +173,6 @@ class HostBlocker: self._config_blocked_hosts) self._blocked_hosts = set() self._done_count = 0 - download_manager = objreg.get('qtnetwork-download-manager') for url in config.val.content.host_blocking.lists: if url.scheme() == 'file': filename = url.toLocalFile() @@ -184,11 +183,7 @@ class HostBlocker: else: self._import_local(filename) else: - fobj = io.BytesIO() - fobj.name = 'adblock: ' + url.host() - target = downloads.FileObjDownloadTarget(fobj) - download = download_manager.get(url, target=target, - auto_remove=True) + download = downloads.download_temp(url) self._in_progress.append(download) download.finished.connect( functools.partial(self._on_download_finished, download)) From 8806c0925e9cb4978fd66d8d9c132ff894a7c10b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:11:16 +0100 Subject: [PATCH 017/117] Expose data and config dir as pathlib.Path --- qutebrowser/extensions/loader.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 4f5e345c6..1aa37e4fe 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -24,6 +24,7 @@ import pkgutil import types import typing import sys +import pathlib import attr @@ -36,7 +37,8 @@ class InitContext: """Context an extension gets in its init hook.""" - data_dir = attr.ib() # type: str + data_dir = attr.ib() # type: pathlib.Path + config_dir = attr.ib() # type: pathlib.Path @attr.s @@ -114,6 +116,12 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: yield ExtensionInfo(name=name) +def _get_init_context() -> InitContext: + """Get an InitContext object.""" + return InitContext(data_dir=pathlib.Path(standarddir.data()), + config_dir=pathlib.Path(standarddir.config())) + + def _load_component(info: ExtensionInfo) -> types.ModuleType: """Load the given extension and run its init hook (if any).""" log.extensions.debug("Importing {}".format(info.name)) @@ -123,7 +131,6 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: if mod_info.init_hook is not None: log.extensions.debug("Running init hook {!r}" .format(mod_info.init_hook.__name__)) - context = InitContext(data_dir=standarddir.data()) - mod_info.init_hook(context) + mod_info.init_hook(_get_init_context()) return mod From 3d6f604739f65bda407c4608ae203e0788c531f6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:29:01 +0100 Subject: [PATCH 018/117] Prepare for moving adblocker to extension API --- qutebrowser/app.py | 5 ---- qutebrowser/browser/adblock.py | 39 +++++++++++++++++++------------- qutebrowser/extensions/loader.py | 6 +++-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 65c7395eb..5bd4b3388 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -468,11 +468,6 @@ def _init_modules(args, crash_handler): log.init.debug("Initializing websettings...") websettings.init(args) - log.init.debug("Initializing adblock...") - host_blocker = adblock.HostBlocker() - host_blocker.read_hosts() - objreg.register('host-blocker', host_blocker) - log.init.debug("Initializing quickmarks...") quickmark_manager = urlmarks.QuickmarkManager(qApp) objreg.register('quickmark-manager', quickmark_manager) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 917942b7f..216e9cc98 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -24,11 +24,12 @@ import os.path import functools import posixpath import zipfile +import logging -from qutebrowser.api import downloads -from qutebrowser.config import config -from qutebrowser.utils import objreg, standarddir, log, message -from qutebrowser.api import cmdutils +from qutebrowser.api import cmdutils, hook, config, message, downloads + + +logger = logging.getLogger('misc') def _guess_zip_filename(zf): @@ -95,18 +96,17 @@ class HostBlocker: _config_hosts_file: The path to a blocked-hosts in ~/.config """ - def __init__(self): + def __init__(self, *, data_dir, config_dir, args): + self._args = args self._blocked_hosts = set() self._config_blocked_hosts = set() self._in_progress = [] self._done_count = 0 - data_dir = standarddir.data() - self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts') + self._local_hosts_file = str(data_dir / 'blocked-hosts') self._update_files() - config_dir = standarddir.config() - self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts') + self._config_hosts_file = str(config_dir / 'blocked-hosts') config.instance.changed.connect(self._update_files) @@ -141,7 +141,7 @@ class HostBlocker: for line in f: target.add(line.strip()) except (OSError, UnicodeDecodeError): - log.misc.exception("Failed to read host blocklist!") + logger.exception("Failed to read host blocklist!") return True @@ -156,9 +156,8 @@ class HostBlocker: self._blocked_hosts) if not found: - args = objreg.get('args') if (config.val.content.host_blocking.lists and - args.basedir is None and + self._args.basedir is None and config.val.content.host_blocking.enabled): message.info("Run :adblock-update to get adblock lists.") @@ -221,7 +220,7 @@ class HostBlocker: try: line = line.decode('utf-8') except UnicodeDecodeError: - log.misc.error("Failed to decode: {!r}".format(line)) + logger.error("Failed to decode: {!r}".format(line)) return False # Remove comments @@ -277,7 +276,7 @@ class HostBlocker: if not ok: error_count += 1 - log.misc.debug("{}: read {} lines".format(byte_io.name, line_count)) + logger.debug("{}: read {} lines".format(byte_io.name, line_count)) if error_count > 0: message.error("adblock: {} read errors for {}".format( error_count, byte_io.name)) @@ -299,7 +298,7 @@ class HostBlocker: except FileNotFoundError: pass except OSError as e: - log.misc.exception("Failed to delete hosts file: {}".format(e)) + logger.exception("Failed to delete hosts file: {}".format(e)) def _on_download_finished(self, download): """Check if all downloads are finished and if so, trigger reading. @@ -318,4 +317,12 @@ class HostBlocker: try: self._on_lists_downloaded() except OSError: - log.misc.exception("Failed to write host block list!") + logger.exception("Failed to write host block list!") + + +@hook.init() +def init(context): + host_blocker = HostBlocker(data_dir=context.data_dir, + config_dir=context.config_dir, + args=context.args) + host_blocker.read_hosts() diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 1aa37e4fe..303eda213 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -29,7 +29,7 @@ import pathlib import attr from qutebrowser import components -from qutebrowser.utils import log, standarddir +from qutebrowser.utils import log, standarddir, objreg @attr.s @@ -39,6 +39,7 @@ class InitContext: data_dir = attr.ib() # type: pathlib.Path config_dir = attr.ib() # type: pathlib.Path + args = attr.ib() # type: argparse.Namespace @attr.s @@ -119,7 +120,8 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), - config_dir=pathlib.Path(standarddir.config())) + config_dir=pathlib.Path(standarddir.config()), + args=objreg.get('args')) def _load_component(info: ExtensionInfo) -> types.ModuleType: From 7ad7623d7338d20c339684117487a98aac589ac8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:39:07 +0100 Subject: [PATCH 019/117] Add request filter API for host blocking Closes https://github.com/qutebrowser/qutebrowser-extensions/issues/8 --- qutebrowser/api/requests.py | 37 +++++++++++++ qutebrowser/browser/adblock.py | 12 +++-- qutebrowser/browser/webengine/interceptor.py | 11 ++-- qutebrowser/browser/webengine/webenginetab.py | 4 +- qutebrowser/browser/webkit/mhtml.py | 6 ++- .../browser/webkit/network/networkmanager.py | 9 ++-- qutebrowser/extensions/requests.py | 53 +++++++++++++++++++ 7 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 qutebrowser/api/requests.py create mode 100644 qutebrowser/extensions/requests.py diff --git a/qutebrowser/api/requests.py b/qutebrowser/api/requests.py new file mode 100644 index 000000000..990faec88 --- /dev/null +++ b/qutebrowser/api/requests.py @@ -0,0 +1,37 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +"""APIs related to intercepting/blocking requests.""" + +import typing + +import attr +from PyQt5.QtCore import QUrl + +from qutebrowser.extensions import requests +# pylint: disable=unused-import +from qutebrowser.extensions.requests import Request + + +def register_filter(reqfilter: requests.RequestFilterType) -> None: + """Register a request filter. + + Whenever a request happens, the filter gets called with a Request object. + """ + requests.register_filter(reqfilter) diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 216e9cc98..e9d171f26 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -119,9 +119,15 @@ class HostBlocker: return False host = url.host() - return ((host in self._blocked_hosts or - host in self._config_blocked_hosts) and - not _is_whitelisted_url(url)) + blocked = ((host in self._blocked_hosts or + host in self._config_blocked_hosts) and + not _is_whitelisted_url(url)) + + if blocked: + logger.info("Request to {} blocked by host blocker." + .format(url.host())) + + return blocked def _read_hosts_file(self, filename, target): """Read hosts from the given filename. diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 516dd0899..863234dc6 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, from qutebrowser.config import config from qutebrowser.browser import shared from qutebrowser.utils import utils, log, debug +from qutebrowser.extensions import requests class RequestInterceptor(QWebEngineUrlRequestInterceptor): """Handle ad blocking and custom headers.""" - def __init__(self, host_blocker, args, parent=None): + def __init__(self, args, parent=None): super().__init__(parent) - self._host_blocker = host_blocker self._args = args def install(self, profile): @@ -84,9 +84,10 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): return # FIXME:qtwebengine only block ads for NavigationTypeOther? - if self._host_blocker.is_blocked(url, first_party): - log.webview.info("Request to {} blocked by host blocker.".format( - url.host())) + request = requests.Request(first_party_url=first_party, + request_url=url) + requests.run_filters(request) + if request.is_blocked: info.block(True) for header, value in shared.custom_headers(url=url): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index a74d866ea..22380cb1f 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -60,10 +60,8 @@ def init(): _qute_scheme_handler.install(webenginesettings.private_profile) log.init.debug("Initializing request interceptor...") - host_blocker = objreg.get('host-blocker') args = objreg.get('args') - req_interceptor = interceptor.RequestInterceptor( - host_blocker, args=args, parent=app) + req_interceptor = interceptor.RequestInterceptor(args=args, parent=app) req_interceptor.install(webenginesettings.default_profile) req_interceptor.install(webenginesettings.private_profile) diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 1ecebed2d..c390ab6b3 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -39,6 +39,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.browser import downloads from qutebrowser.browser.webkit import webkitelem from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils +from qutebrowser.extensions import requests @attr.s @@ -354,8 +355,9 @@ class _Downloader: # qute, see the comments/discussion on # https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987 # and https://github.com/qutebrowser/qutebrowser/issues/1053 - host_blocker = objreg.get('host-blocker') - if host_blocker.is_blocked(url): + request = requests.Request(first_party_url=None, request_url=url) + requests.run_filters(request) + if request.is_blocked: log.downloads.debug("Skipping {}, host-blocked".format(url)) # We still need an empty file in the output, QWebView can be pretty # picky about displaying a file correctly when not all assets are diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 2ca1ae0d9..9562ba918 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -38,6 +38,7 @@ if MYPY: from qutebrowser.utils import (message, log, usertypes, utils, objreg, urlutils, debug) from qutebrowser.browser import shared +from qutebrowser.extensions import requests from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, filescheme) @@ -405,10 +406,10 @@ class NetworkManager(QNetworkAccessManager): # the webpage shutdown here. current_url = QUrl() - host_blocker = objreg.get('host-blocker') - if host_blocker.is_blocked(req.url(), current_url): - log.webview.info("Request to {} blocked by host blocker.".format( - req.url().host())) + request = requests.Request(first_party_url=current_url, + request_url=req.url()) + requests.run_filters(request) + if request.is_blocked: return networkreply.ErrorNetworkReply( req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied, self) diff --git a/qutebrowser/extensions/requests.py b/qutebrowser/extensions/requests.py new file mode 100644 index 000000000..8364dc76d --- /dev/null +++ b/qutebrowser/extensions/requests.py @@ -0,0 +1,53 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +"""Infrastructure for filtering requests.""" + +import typing + +import attr + + +@attr.s +class Request: + + """A request which can be blocked.""" + + first_party_url = attr.ib() # type: QUrl + request_url = attr.ib() # type: QUrl + is_blocked = attr.ib(False) # type: bool + + def block(self): + """Block this request.""" + self.is_blocked = True + + +RequestFilterType = typing.Callable[[Request], None] + + +_request_filters = [] # type: typing.List[RequestFilterType] + + +def register_filter(reqfilter: RequestFilterType) -> None: + _request_filters.append(reqfilter) + + +def run_filters(info): + for reqfilter in _request_filters: + reqfilter(info) From 9764472fd81b29842dd2cab813f4849d0474d4ed Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:44:04 +0100 Subject: [PATCH 020/117] Move adblock from browser/ to components/ --- qutebrowser/app.py | 2 +- qutebrowser/{browser => components}/adblock.py | 0 scripts/hostblock_blame.py | 2 +- tests/unit/{browser => components}/test_adblock.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename qutebrowser/{browser => components}/adblock.py (100%) rename tests/unit/{browser => components}/test_adblock.py (99%) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 5bd4b3388..9c1ce9991 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -63,7 +63,7 @@ from qutebrowser.completion.models import miscmodels from qutebrowser.commands import runners from qutebrowser.api import cmdutils from qutebrowser.config import config, websettings, configfiles, configinit -from qutebrowser.browser import (urlmarks, adblock, history, browsertab, +from qutebrowser.browser import (urlmarks, history, browsertab, qtnetworkdownloads, downloads, greasemonkey) from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import cookies, cache diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/components/adblock.py similarity index 100% rename from qutebrowser/browser/adblock.py rename to qutebrowser/components/adblock.py diff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py index 2f68d2961..e5508f515 100644 --- a/scripts/hostblock_blame.py +++ b/scripts/hostblock_blame.py @@ -27,7 +27,7 @@ import os.path import urllib.request sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) -from qutebrowser.browser import adblock +from qutebrowser.components import adblock from qutebrowser.config import configdata diff --git a/tests/unit/browser/test_adblock.py b/tests/unit/components/test_adblock.py similarity index 99% rename from tests/unit/browser/test_adblock.py rename to tests/unit/components/test_adblock.py index e18c990cc..e354705dd 100644 --- a/tests/unit/browser/test_adblock.py +++ b/tests/unit/components/test_adblock.py @@ -28,7 +28,7 @@ import pytest from PyQt5.QtCore import QUrl -from qutebrowser.browser import adblock +from qutebrowser.components import adblock from qutebrowser.utils import urlmatch from tests.helpers import utils From 42790e762342ec8f2b00f554d675dce8ff588555 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:45:24 +0100 Subject: [PATCH 021/117] Expose config.change_filter --- qutebrowser/api/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index 6558cf42a..ad0338dd2 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -25,5 +25,7 @@ MYPY = False if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.config import config +# pylint: disable=unused-import +from qutebrowser.config.config import change_filter val = typing.cast('config.ConfigContainer', None) From 1b1872e46430061eac70ee70cad0ea58ddbba7e7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:53:42 +0100 Subject: [PATCH 022/117] Expose a config_changed signal to extensions --- qutebrowser/app.py | 1 + qutebrowser/components/adblock.py | 7 +++---- qutebrowser/extensions/loader.py | 24 +++++++++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 9c1ce9991..2b6896b76 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -165,6 +165,7 @@ def init(args, crash_handler): qApp.setQuitOnLastWindowClosed(False) _init_icon() + loader.init() loader.load_components() try: _init_modules(args, crash_handler) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index e9d171f26..77e7a7694 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -104,12 +104,10 @@ class HostBlocker: self._done_count = 0 self._local_hosts_file = str(data_dir / 'blocked-hosts') - self._update_files() + self.update_files() self._config_hosts_file = str(config_dir / 'blocked-hosts') - config.instance.changed.connect(self._update_files) - def is_blocked(self, url, first_party_url=None): """Check if the given URL (as QUrl) is blocked.""" if first_party_url is not None and not first_party_url.isValid(): @@ -296,7 +294,7 @@ class HostBlocker: len(self._blocked_hosts), self._done_count)) @config.change_filter('content.host_blocking.lists') - def _update_files(self): + def update_files(self): """Update files when the config changed.""" if not config.val.content.host_blocking.lists: try: @@ -331,4 +329,5 @@ def init(context): host_blocker = HostBlocker(data_dir=context.data_dir, config_dir=context.config_dir, args=context.args) + context.signals.config_changed.connect(host_blocker.update_files) host_blocker.read_hosts() diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 303eda213..e7b93967b 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -28,7 +28,10 @@ import pathlib import attr +from PyQt5.QtCore import pyqtSignal, QObject + from qutebrowser import components +from qutebrowser.config import config from qutebrowser.utils import log, standarddir, objreg @@ -40,6 +43,14 @@ class InitContext: data_dir = attr.ib() # type: pathlib.Path config_dir = attr.ib() # type: pathlib.Path args = attr.ib() # type: argparse.Namespace + signals = attr.ib() # type: ExtensionSignals + + +class ExtensionSignals(QObject): + + """Signals exposed to an extension.""" + + config_changed = pyqtSignal(str) @attr.s @@ -61,6 +72,12 @@ class ExtensionInfo: name = attr.ib() # type: str +# Global extension signals, shared between all extensions. +# At some point we might want to make this per-extension, but then we'll need +# to find out what to set as its Qt parent so it's kept alive. +_extension_signals = ExtensionSignals() + + def add_module_info(module: types.ModuleType) -> ModuleInfo: """Add ModuleInfo to a module (if not added yet).""" # pylint: disable=protected-access @@ -121,7 +138,8 @@ def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), config_dir=pathlib.Path(standarddir.config()), - args=objreg.get('args')) + args=objreg.get('args'), + signals=_extension_signals) def _load_component(info: ExtensionInfo) -> types.ModuleType: @@ -136,3 +154,7 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: mod_info.init_hook(_get_init_context()) return mod + + +def init() -> None: + config.instance.changed.connect(_extension_signals.config_changed) From 58d179302edcf9c4be980494494cdde55f6e02f0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:55:28 +0100 Subject: [PATCH 023/117] Add api.config.get --- qutebrowser/api/config.py | 12 ++++++++---- qutebrowser/components/adblock.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index ad0338dd2..c2831de40 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -21,11 +21,15 @@ import typing -MYPY = False -if MYPY: - # pylint: disable=unused-import,useless-suppression - from qutebrowser.config import config +from PyQt5.QtCore import QUrl + +from qutebrowser.config import config # pylint: disable=unused-import from qutebrowser.config.config import change_filter val = typing.cast('config.ConfigContainer', None) + + +def get(name: str, url: QUrl = None) -> typing.Any: + """Get a value from the config based on a string name.""" + return config.instance.get(name, url) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 77e7a7694..492acb0a8 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -112,8 +112,8 @@ class HostBlocker: """Check if the given URL (as QUrl) is blocked.""" if first_party_url is not None and not first_party_url.isValid(): first_party_url = None - if not config.instance.get('content.host_blocking.enabled', - url=first_party_url): + if not config.get('content.host_blocking.enabled', + url=first_party_url): return False host = url.host() From a146ce865b4fdfa27086596c011dd5112a18a61a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 14:59:00 +0100 Subject: [PATCH 024/117] Register host blocker as request filter --- qutebrowser/components/adblock.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 492acb0a8..264f55cda 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -26,7 +26,8 @@ import posixpath import zipfile import logging -from qutebrowser.api import cmdutils, hook, config, message, downloads +from qutebrowser.api import (cmdutils, hook, config, message, downloads, + requests) logger = logging.getLogger('misc') @@ -108,24 +109,28 @@ class HostBlocker: self._config_hosts_file = str(config_dir / 'blocked-hosts') - def is_blocked(self, url, first_party_url=None): - """Check if the given URL (as QUrl) is blocked.""" - if first_party_url is not None and not first_party_url.isValid(): + def filter_request(self, info: requests.Request) -> None: + """Block the given request if necessary.""" + if info.first_party_url is None: first_party_url = None + elif not info.first_party_url.isValid(): + first_party_url = None + else: + first_party_url = info.first_party_url + if not config.get('content.host_blocking.enabled', url=first_party_url): return False - host = url.host() + host = info.request_url.host() blocked = ((host in self._blocked_hosts or host in self._config_blocked_hosts) and - not _is_whitelisted_url(url)) + not _is_whitelisted_url(info.request_url)) if blocked: logger.info("Request to {} blocked by host blocker." - .format(url.host())) - - return blocked + .format(info.request_url.host())) + info.block() def _read_hosts_file(self, filename, target): """Read hosts from the given filename. @@ -329,5 +334,7 @@ def init(context): host_blocker = HostBlocker(data_dir=context.data_dir, config_dir=context.config_dir, args=context.args) - context.signals.config_changed.connect(host_blocker.update_files) host_blocker.read_hosts() + + context.signals.config_changed.connect(host_blocker.update_files) + requests.register_filter(host_blocker.filter_request) From 93a36c857251bb4d1a4e80804b2c0a4d531d3888 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 15:01:52 +0100 Subject: [PATCH 025/117] Fix :adblock-update After #640 we can likely fix this up again. --- qutebrowser/components/adblock.py | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 264f55cda..85d8fe40b 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -31,6 +31,7 @@ from qutebrowser.api import (cmdutils, hook, config, message, downloads, logger = logging.getLogger('misc') +_host_blocker = None def _guess_zip_filename(zf): @@ -170,13 +171,8 @@ class HostBlocker: config.val.content.host_blocking.enabled): message.info("Run :adblock-update to get adblock lists.") - @cmdutils.register(instance='host-blocker') def adblock_update(self): - """Update the adblock block lists. - - This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded - host lists and re-reads `~/.config/qutebrowser/blocked-hosts`. - """ + """Update the adblock block lists.""" self._read_hosts_file(self._config_hosts_file, self._config_blocked_hosts) self._blocked_hosts = set() @@ -329,12 +325,25 @@ class HostBlocker: logger.exception("Failed to write host block list!") +@cmdutils.register() +def adblock_update(): + """Update the adblock block lists. + + This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded + host lists and re-reads `~/.config/qutebrowser/blocked-hosts`. + """ + # FIXME: As soon as we can register instances again, we should move this + # back to the class. + _host_blocker.adblock_update() + + @hook.init() def init(context): - host_blocker = HostBlocker(data_dir=context.data_dir, - config_dir=context.config_dir, - args=context.args) - host_blocker.read_hosts() + global _host_blocker + _host_blocker = HostBlocker(data_dir=context.data_dir, + config_dir=context.config_dir, + args=context.args) + _host_blocker.read_hosts() - context.signals.config_changed.connect(host_blocker.update_files) - requests.register_filter(host_blocker.filter_request) + context.signals.config_changed.connect(_host_blocker.update_files) + requests.register_filter(_host_blocker.filter_request) From 6001640a8a194c41df9b510b0c2949776adff30e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 15:24:15 +0100 Subject: [PATCH 026/117] Guard against double init hooks --- qutebrowser/api/hook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index e7e0de88d..ddec49e5f 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -33,5 +33,7 @@ class init: # noqa: N801,N806 pylint: disable=invalid-name def __call__(self, func: typing.Callable) -> typing.Callable: module = importlib.import_module(func.__module__) info = loader.add_module_info(module) + if info.init_hook is not None: + raise ValueError("init hook is already registered!") info.init_hook = func return func From 3b53270ee379088b6feeb4d0d81f7ea9707d2b8b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 15:40:19 +0100 Subject: [PATCH 027/117] Use hook system for config_changed hook --- qutebrowser/api/config.py | 2 -- qutebrowser/api/hook.py | 21 ++++++++++++++-- qutebrowser/components/adblock.py | 8 +++--- qutebrowser/config/config.py | 6 ++--- qutebrowser/extensions/loader.py | 42 ++++++++++++++++++------------- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index c2831de40..4a5d73936 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -24,8 +24,6 @@ import typing from PyQt5.QtCore import QUrl from qutebrowser.config import config -# pylint: disable=unused-import -from qutebrowser.config.config import change_filter val = typing.cast('config.ConfigContainer', None) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index ddec49e5f..a975438ea 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -26,14 +26,31 @@ import typing from qutebrowser.extensions import loader +def _add_module_info(func: typing.Callable) -> loader.ModuleInfo: + """Add module info to the given function.""" + module = importlib.import_module(func.__module__) + return loader.add_module_info(module) + + class init: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to mark a function to run when initializing.""" def __call__(self, func: typing.Callable) -> typing.Callable: - module = importlib.import_module(func.__module__) - info = loader.add_module_info(module) + info = _add_module_info(func) if info.init_hook is not None: raise ValueError("init hook is already registered!") info.init_hook = func return func + + +class config_changed: + + """Decorator to get notified about changed configs.""" + + def __init__(self, option_filter=None): + self._filter = option_filter + + def __call__(self, func: typing.Callable) -> typing.Callable: + info = _add_module_info(func) + info.config_changed_hooks.append((self._filter, func)) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 85d8fe40b..ae71722d6 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -294,7 +294,6 @@ class HostBlocker: message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count)) - @config.change_filter('content.host_blocking.lists') def update_files(self): """Update files when the config changed.""" if not config.val.content.host_blocking.lists: @@ -337,6 +336,11 @@ def adblock_update(): _host_blocker.adblock_update() +@hook.config_changed('content.host_blocking.lists') +def on_config_changed(): + _host_blocker.update_files() + + @hook.init() def init(context): global _host_blocker @@ -344,6 +348,4 @@ def init(context): config_dir=context.config_dir, args=context.args) _host_blocker.read_hosts() - - context.signals.config_changed.connect(_host_blocker.update_files) requests.register_filter(_host_blocker.filter_request) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 80826beeb..201b87fde 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -86,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name not configdata.is_valid_prefix(self._option)): raise configexc.NoOptionError(self._option) - def _check_match(self, option: typing.Optional[str]) -> bool: + def check_match(self, option: typing.Optional[str]) -> bool: """Check if the given option matches the filter.""" if option is None: # Called directly, not from a config change event. @@ -119,7 +119,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name @functools.wraps(func) def func_wrapper(option: str = None) -> typing.Any: """Call the underlying function.""" - if self._check_match(option): + if self.check_match(option): return func() return None return func_wrapper @@ -128,7 +128,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name def meth_wrapper(wrapper_self: typing.Any, option: str = None) -> typing.Any: """Call the underlying function.""" - if self._check_match(option): + if self.check_match(option): return func(wrapper_self) return None return meth_wrapper diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index e7b93967b..bc4494bae 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -28,13 +28,17 @@ import pathlib import attr -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSlot, QObject from qutebrowser import components from qutebrowser.config import config from qutebrowser.utils import log, standarddir, objreg +# ModuleInfo objects for all loaded plugins +_module_infos = [] + + @attr.s class InitContext: @@ -43,14 +47,6 @@ class InitContext: data_dir = attr.ib() # type: pathlib.Path config_dir = attr.ib() # type: pathlib.Path args = attr.ib() # type: argparse.Namespace - signals = attr.ib() # type: ExtensionSignals - - -class ExtensionSignals(QObject): - - """Signals exposed to an extension.""" - - config_changed = pyqtSignal(str) @attr.s @@ -61,7 +57,11 @@ class ModuleInfo: This gets used by qutebrowser.api.hook. """ + _ConfigChangedHooksType = typing.List[typing.Tuple[str, typing.Callable]] + init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] + config_changed_hooks = attr.ib( + attr.Factory(list)) # type: _ConfigChangedHooksType @attr.s @@ -72,12 +72,6 @@ class ExtensionInfo: name = attr.ib() # type: str -# Global extension signals, shared between all extensions. -# At some point we might want to make this per-extension, but then we'll need -# to find out what to set as its Qt parent so it's kept alive. -_extension_signals = ExtensionSignals() - - def add_module_info(module: types.ModuleType) -> ModuleInfo: """Add ModuleInfo to a module (if not added yet).""" # pylint: disable=protected-access @@ -138,8 +132,7 @@ def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), config_dir=pathlib.Path(standarddir.config()), - args=objreg.get('args'), - signals=_extension_signals) + args=objreg.get('args')) def _load_component(info: ExtensionInfo) -> types.ModuleType: @@ -153,8 +146,21 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: .format(mod_info.init_hook.__name__)) mod_info.init_hook(_get_init_context()) + _module_infos.append(mod_info) + return mod +@pyqtSlot(str) +def _on_config_changed(changed_name: str) -> None: + """Call config_changed hooks if the config changed.""" + for mod_info in _module_infos: + for option, hook in mod_info.config_changed_hooks: + cfilter = config.change_filter(option) + cfilter.validate() + if cfilter.check_match(changed_name): + hook() + + def init() -> None: - config.instance.changed.connect(_extension_signals.config_changed) + config.instance.changed.connect(_on_config_changed) From a96c6efc34cbb8d612ad651b858650e1f6fb2dd5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 15:52:00 +0100 Subject: [PATCH 028/117] Add types to components.adblock --- qutebrowser/components/adblock.py | 80 ++++++++++++++----------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index ae71722d6..aad802bd2 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -25,21 +25,22 @@ import functools import posixpath import zipfile import logging +import typing +import pathlib +import argparse + +from PyQt5.QtCore import QUrl from qutebrowser.api import (cmdutils, hook, config, message, downloads, - requests) + requests, apitypes) logger = logging.getLogger('misc') -_host_blocker = None +_host_blocker = typing.cast('HostBlocker', None) -def _guess_zip_filename(zf): - """Guess which file to use inside a zip file. - - Args: - zf: A ZipFile instance. - """ +def _guess_zip_filename(zf: zipfile.ZipFile) -> str: + """Guess which file to use inside a zip file.""" files = zf.namelist() if len(files) == 1: return files[0] @@ -50,7 +51,7 @@ def _guess_zip_filename(zf): raise FileNotFoundError("No hosts file found in zip") -def get_fileobj(byte_io): +def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]: """Get a usable file object to read the hosts file from.""" byte_io.seek(0) # rewind downloaded file if zipfile.is_zipfile(byte_io): @@ -63,24 +64,19 @@ def get_fileobj(byte_io): return byte_io -def _is_whitelisted_url(url): - """Check if the given URL is on the adblock whitelist. - - Args: - url: The URL to check as QUrl. - """ +def _is_whitelisted_url(url: QUrl) -> bool: + """Check if the given URL is on the adblock whitelist.""" for pattern in config.val.content.host_blocking.whitelist: if pattern.matches(url): return True return False -class _FakeDownload: +class _FakeDownload(downloads.TempDownload): """A download stub to use on_download_finished with local files.""" - def __init__(self, fileobj): - self.basename = os.path.basename(fileobj.name) + def __init__(self, fileobj: typing.IO[bytes]) -> None: self.fileobj = fileobj self.successful = True @@ -98,11 +94,12 @@ class HostBlocker: _config_hosts_file: The path to a blocked-hosts in ~/.config """ - def __init__(self, *, data_dir, config_dir, args): + def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path, + args: argparse.Namespace) -> None: self._args = args - self._blocked_hosts = set() - self._config_blocked_hosts = set() - self._in_progress = [] + self._blocked_hosts = set() # type: typing.Set[str] + self._config_blocked_hosts = set() # type: typing.Set[str] + self._in_progress = [] # type: typing.List[downloads.TempDownload] self._done_count = 0 self._local_hosts_file = str(data_dir / 'blocked-hosts') @@ -121,7 +118,7 @@ class HostBlocker: if not config.get('content.host_blocking.enabled', url=first_party_url): - return False + return host = info.request_url.host() blocked = ((host in self._blocked_hosts or @@ -133,7 +130,7 @@ class HostBlocker: .format(info.request_url.host())) info.block() - def _read_hosts_file(self, filename, target): + def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool: """Read hosts from the given filename. Args: @@ -155,7 +152,7 @@ class HostBlocker: return True - def read_hosts(self): + def read_hosts(self) -> None: """Read hosts from the existing blocked-hosts file.""" self._blocked_hosts = set() @@ -171,7 +168,7 @@ class HostBlocker: config.val.content.host_blocking.enabled): message.info("Run :adblock-update to get adblock lists.") - def adblock_update(self): + def adblock_update(self) -> None: """Update the adblock block lists.""" self._read_hosts_file(self._config_hosts_file, self._config_blocked_hosts) @@ -192,7 +189,7 @@ class HostBlocker: download.finished.connect( functools.partial(self._on_download_finished, download)) - def _import_local(self, filename): + def _import_local(self, filename: str) -> None: """Adds the contents of a file to the blocklist. Args: @@ -208,24 +205,24 @@ class HostBlocker: self._in_progress.append(download) self._on_download_finished(download) - def _parse_line(self, line): + def _parse_line(self, raw_line: bytes) -> bool: """Parse a line from a host file. Args: - line: The bytes object to parse. + raw_line: The bytes object to parse. Returns: True if parsing succeeded, False otherwise. """ - if line.startswith(b'#'): + if raw_line.startswith(b'#'): # Ignoring comments early so we don't have to care about # encoding errors in them. return True try: - line = line.decode('utf-8') + line = raw_line.decode('utf-8') except UnicodeDecodeError: - logger.error("Failed to decode: {!r}".format(line)) + logger.error("Failed to decode: {!r}".format(raw_line)) return False # Remove comments @@ -256,14 +253,11 @@ class HostBlocker: return True - def _merge_file(self, byte_io): + def _merge_file(self, byte_io: io.BytesIO) -> None: """Read and merge host files. Args: byte_io: The BytesIO object of the completed download. - - Return: - A set of the merged hosts. """ error_count = 0 line_count = 0 @@ -286,7 +280,7 @@ class HostBlocker: message.error("adblock: {} read errors for {}".format( error_count, byte_io.name)) - def _on_lists_downloaded(self): + def _on_lists_downloaded(self) -> None: """Install block lists after files have been downloaded.""" with open(self._local_hosts_file, 'w', encoding='utf-8') as f: for host in sorted(self._blocked_hosts): @@ -294,7 +288,7 @@ class HostBlocker: message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count)) - def update_files(self): + def update_files(self) -> None: """Update files when the config changed.""" if not config.val.content.host_blocking.lists: try: @@ -304,11 +298,11 @@ class HostBlocker: except OSError as e: logger.exception("Failed to delete hosts file: {}".format(e)) - def _on_download_finished(self, download): + def _on_download_finished(self, download: downloads.TempDownload) -> None: """Check if all downloads are finished and if so, trigger reading. Arguments: - download: The finished DownloadItem. + download: The finished download. """ self._in_progress.remove(download) if download.successful: @@ -325,7 +319,7 @@ class HostBlocker: @cmdutils.register() -def adblock_update(): +def adblock_update() -> None: """Update the adblock block lists. This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded @@ -337,12 +331,12 @@ def adblock_update(): @hook.config_changed('content.host_blocking.lists') -def on_config_changed(): +def on_config_changed() -> None: _host_blocker.update_files() @hook.init() -def init(context): +def init(context: apitypes.InitContext) -> None: global _host_blocker _host_blocker = HostBlocker(data_dir=context.data_dir, config_dir=context.config_dir, From b41005d487812bc9a9eeb07bfc3b9a4da7e72b32 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 15:56:05 +0100 Subject: [PATCH 029/117] Fix mypy issues --- qutebrowser/api/hook.py | 3 ++- qutebrowser/extensions/loader.py | 17 +++++++++++++---- qutebrowser/extensions/requests.py | 9 +++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index a975438ea..bd8e2a635 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -48,9 +48,10 @@ class config_changed: """Decorator to get notified about changed configs.""" - def __init__(self, option_filter=None): + def __init__(self, option_filter: str = None) -> None: self._filter = option_filter def __call__(self, func: typing.Callable) -> typing.Callable: info = _add_module_info(func) info.config_changed_hooks.append((self._filter, func)) + return func diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index bc4494bae..be7beeb11 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -34,6 +34,11 @@ from qutebrowser import components from qutebrowser.config import config from qutebrowser.utils import log, standarddir, objreg +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + import argparse + # ModuleInfo objects for all loaded plugins _module_infos = [] @@ -57,7 +62,8 @@ class ModuleInfo: This gets used by qutebrowser.api.hook. """ - _ConfigChangedHooksType = typing.List[typing.Tuple[str, typing.Callable]] + _ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str], + typing.Callable]] init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] config_changed_hooks = attr.ib( @@ -156,10 +162,13 @@ def _on_config_changed(changed_name: str) -> None: """Call config_changed hooks if the config changed.""" for mod_info in _module_infos: for option, hook in mod_info.config_changed_hooks: - cfilter = config.change_filter(option) - cfilter.validate() - if cfilter.check_match(changed_name): + if option is None: hook() + else: + cfilter = config.change_filter(option) + cfilter.validate() + if cfilter.check_match(changed_name): + hook() def init() -> None: diff --git a/qutebrowser/extensions/requests.py b/qutebrowser/extensions/requests.py index 8364dc76d..952c830dc 100644 --- a/qutebrowser/extensions/requests.py +++ b/qutebrowser/extensions/requests.py @@ -23,6 +23,11 @@ import typing import attr +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + from PyQt5.QtCore import QUrl + @attr.s class Request: @@ -33,7 +38,7 @@ class Request: request_url = attr.ib() # type: QUrl is_blocked = attr.ib(False) # type: bool - def block(self): + def block(self) -> None: """Block this request.""" self.is_blocked = True @@ -48,6 +53,6 @@ def register_filter(reqfilter: RequestFilterType) -> None: _request_filters.append(reqfilter) -def run_filters(info): +def run_filters(info: Request) -> None: for reqfilter in _request_filters: reqfilter(info) From 4e99caafb9f9772155bbf5de18e8bad60b2127e0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 16:00:31 +0100 Subject: [PATCH 030/117] Skip hooks for vulture/docs --- qutebrowser/extensions/loader.py | 21 ++++++++++++++++----- scripts/dev/run_vulture.py | 2 +- scripts/dev/src2asciidoc.py | 2 +- tests/unit/extensions/test_loader.py | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index be7beeb11..338449c5b 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -65,6 +65,7 @@ class ModuleInfo: _ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str], typing.Callable]] + skip_hooks = attr.ib(False) # type: bool init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] config_changed_hooks = attr.ib( attr.Factory(list)) # type: _ConfigChangedHooksType @@ -86,10 +87,10 @@ def add_module_info(module: types.ModuleType) -> ModuleInfo: return module.__qute_module_info # type: ignore -def load_components() -> None: +def load_components(*, skip_hooks=False) -> None: """Load everything from qutebrowser.components.""" for info in walk_components(): - _load_component(info) + _load_component(info, skip_hooks=skip_hooks) def walk_components() -> typing.Iterator[ExtensionInfo]: @@ -141,13 +142,21 @@ def _get_init_context() -> InitContext: args=objreg.get('args')) -def _load_component(info: ExtensionInfo) -> types.ModuleType: - """Load the given extension and run its init hook (if any).""" +def _load_component(info: ExtensionInfo, *, skip_hooks) -> types.ModuleType: + """Load the given extension and run its init hook (if any). + + Args: + skip_hooks: Whether to skip all hooks for this module. + This is used to only run @cmdutils.register decorators. + """ log.extensions.debug("Importing {}".format(info.name)) mod = importlib.import_module(info.name) mod_info = add_module_info(mod) - if mod_info.init_hook is not None: + if skip_hooks: + mod_info.skip_hooks = True + + if mod_info.init_hook is not None and not skip_hooks: log.extensions.debug("Running init hook {!r}" .format(mod_info.init_hook.__name__)) mod_info.init_hook(_get_init_context()) @@ -161,6 +170,8 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: def _on_config_changed(changed_name: str) -> None: """Call config_changed hooks if the config changed.""" for mod_info in _module_infos: + if mod_info.skip_hooks: + continue for option, hook in mod_info.config_changed_hooks: if option is None: hook() diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 7874f6a79..51662f3c9 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -44,7 +44,7 @@ from qutebrowser.config import configtypes def whitelist_generator(): # noqa """Generator which yields lines to add to a vulture whitelist.""" - loader.load_components() + loader.load_components(skip_hooks=True) # qutebrowser commands for cmd in objects.commands.values(): diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index f0536c045..1ba272fba 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -550,7 +550,7 @@ def regenerate_cheatsheet(): def main(): """Regenerate all documentation.""" utils.change_cwd() - loader.load_components() + loader.load_components(skip_hooks=True) print("Generating manpage...") regenerate_manpage('doc/qutebrowser.1.asciidoc') print("Generating settings help...") diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py index e0c1912fa..710b1ce9c 100644 --- a/tests/unit/extensions/test_loader.py +++ b/tests/unit/extensions/test_loader.py @@ -43,7 +43,7 @@ def test_load_component(monkeypatch): monkeypatch.setattr(objects, 'commands', {}) info = loader.ExtensionInfo(name='qutebrowser.components.scrollcommands') - module = loader._load_component(info) + module = loader._load_component(info, skip_hooks=True) assert hasattr(module, 'scroll_to_perc') assert 'scroll-to-perc' in objects.commands From 35a3fe029d381053c12b44b5f1d111cfabf3f047 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 16:05:17 +0100 Subject: [PATCH 031/117] Fix lint --- .flake8 | 1 + qutebrowser/api/downloads.py | 4 ++++ qutebrowser/api/hook.py | 4 +++- qutebrowser/api/requests.py | 5 ----- qutebrowser/components/adblock.py | 4 +++- qutebrowser/extensions/loader.py | 2 +- scripts/dev/run_vulture.py | 3 +++ 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index 8c03ef729..7a783a4b0 100644 --- a/.flake8 +++ b/.flake8 @@ -46,6 +46,7 @@ ignore = min-version = 3.4.0 max-complexity = 12 per-file-ignores = + /qutebrowser/api/hook.py : N801 /tests/**/*.py : D100,D101,D401 /tests/unit/browser/test_history.py : N806 /tests/helpers/fixtures.py : N806 diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py index f743f5d7a..82c68d0bd 100644 --- a/qutebrowser/api/downloads.py +++ b/qutebrowser/api/downloads.py @@ -18,6 +18,9 @@ # along with qutebrowser. If not, see . +"""APIs related to downloading files.""" + + import io from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl @@ -33,6 +36,7 @@ class TempDownload(QObject): finished = pyqtSignal() def __init__(self, item: qtnetworkdownloads.DownloadItem) -> None: + super().__init__() self._item = item self._item.finished.connect(self._on_download_finished) self.successful = False diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index bd8e2a635..3f06121da 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# pylint: disable=invalid-name + """Hooks for extensions.""" import importlib @@ -32,7 +34,7 @@ def _add_module_info(func: typing.Callable) -> loader.ModuleInfo: return loader.add_module_info(module) -class init: # noqa: N801,N806 pylint: disable=invalid-name +class init: """Decorator to mark a function to run when initializing.""" diff --git a/qutebrowser/api/requests.py b/qutebrowser/api/requests.py index 990faec88..5c23418b4 100644 --- a/qutebrowser/api/requests.py +++ b/qutebrowser/api/requests.py @@ -19,11 +19,6 @@ """APIs related to intercepting/blocking requests.""" -import typing - -import attr -from PyQt5.QtCore import QUrl - from qutebrowser.extensions import requests # pylint: disable=unused-import from qutebrowser.extensions.requests import Request diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index aad802bd2..706620948 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -76,7 +76,8 @@ class _FakeDownload(downloads.TempDownload): """A download stub to use on_download_finished with local files.""" - def __init__(self, fileobj: typing.IO[bytes]) -> None: + def __init__(self, # pylint: disable=super-init-not-called + fileobj: typing.IO[bytes]) -> None: self.fileobj = fileobj self.successful = True @@ -337,6 +338,7 @@ def on_config_changed() -> None: @hook.init() def init(context: apitypes.InitContext) -> None: + """Initialize the host blocker.""" global _host_blocker _host_blocker = HostBlocker(data_dir=context.data_dir, config_dir=context.config_dir, diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 338449c5b..1a9b3d858 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -28,7 +28,7 @@ import pathlib import attr -from PyQt5.QtCore import pyqtSlot, QObject +from PyQt5.QtCore import pyqtSlot from qutebrowser import components from qutebrowser.config import config diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 51662f3c9..f9262c946 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -130,6 +130,9 @@ def whitelist_generator(): # noqa yield 'scripts.get_coredumpctl_traces.Line.gid' yield 'scripts.importer.import_moz_places.places.row_factory' + # component hooks + yield 'qutebrowser.components.adblock.on_config_changed' + def filter_func(item): """Check if a missing function should be filtered or not. From 8508928f3de8996b928ab6be99e5c083c7f0f78a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 16:07:40 +0100 Subject: [PATCH 032/117] Use bool instead of passing args --- qutebrowser/components/adblock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 706620948..33069ce2d 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -27,7 +27,6 @@ import zipfile import logging import typing import pathlib -import argparse from PyQt5.QtCore import QUrl @@ -93,11 +92,12 @@ class HostBlocker: _done_count: How many files have been read successfully. _local_hosts_file: The path to the blocked-hosts file. _config_hosts_file: The path to a blocked-hosts in ~/.config + _has_basedir: Whether a custom --basedir is set. """ def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path, - args: argparse.Namespace) -> None: - self._args = args + has_basedir: bool = False) -> None: + self._has_basedir = has_basedir self._blocked_hosts = set() # type: typing.Set[str] self._config_blocked_hosts = set() # type: typing.Set[str] self._in_progress = [] # type: typing.List[downloads.TempDownload] @@ -165,7 +165,7 @@ class HostBlocker: if not found: if (config.val.content.host_blocking.lists and - self._args.basedir is None and + not self._has_basedir and config.val.content.host_blocking.enabled): message.info("Run :adblock-update to get adblock lists.") @@ -342,6 +342,6 @@ def init(context: apitypes.InitContext) -> None: global _host_blocker _host_blocker = HostBlocker(data_dir=context.data_dir, config_dir=context.config_dir, - args=context.args) + has_basedir=context.args.basedir is not None) _host_blocker.read_hosts() requests.register_filter(_host_blocker.filter_request) From 007250033a3f26a5f76cf5164c89de1eec0f95b7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 16:08:43 +0100 Subject: [PATCH 033/117] Remove HostBlockerStub --- tests/helpers/fixtures.py | 13 ++----------- tests/helpers/stubs.py | 11 ----------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index fac23ae1c..b45427043 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -190,8 +190,8 @@ def testdata_scheme(qapp): @pytest.fixture def web_tab_setup(qtbot, tab_registry, session_manager_stub, - greasemonkey_manager, fake_args, host_blocker_stub, - config_stub, testdata_scheme): + greasemonkey_manager, fake_args, config_stub, + testdata_scheme): """Shared setup for webkit_tab/webengine_tab.""" # Make sure error logging via JS fails tests config_stub.val.content.javascript.log = { @@ -328,15 +328,6 @@ def key_config_stub(config_stub, monkeypatch): return keyconf -@pytest.fixture -def host_blocker_stub(stubs): - """Fixture which provides a fake host blocker object.""" - stub = stubs.HostBlockerStub() - objreg.register('host-blocker', stub) - yield stub - objreg.delete('host-blocker') - - @pytest.fixture def quickmark_manager_stub(stubs): """Fixture which provides a fake quickmark manager object.""" diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 89330ab64..38d82c004 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -459,17 +459,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub): self.delete(key) -class HostBlockerStub: - - """Stub for the host-blocker object.""" - - def __init__(self): - self.blocked_hosts = set() - - def is_blocked(self, url, first_party_url=None): - return url in self.blocked_hosts - - class SessionManagerStub: """Stub for the session-manager object.""" From 7b1bcea30650578e28f9672835c34eb235605f4e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 16:25:36 +0100 Subject: [PATCH 034/117] Bring back separate is_blocked method --- qutebrowser/components/adblock.py | 26 +++--- tests/helpers/fixtures.py | 2 + tests/unit/components/test_adblock.py | 109 +++++++++++--------------- 3 files changed, 62 insertions(+), 75 deletions(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 33069ce2d..d279f1c03 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -108,25 +108,25 @@ class HostBlocker: self._config_hosts_file = str(config_dir / 'blocked-hosts') - def filter_request(self, info: requests.Request) -> None: - """Block the given request if necessary.""" - if info.first_party_url is None: + def _is_blocked(self, request_url: QUrl, + first_party_url: QUrl = None) -> None: + """Check whether the given request is blocked.""" + if first_party_url is not None and not first_party_url.isValid(): first_party_url = None - elif not info.first_party_url.isValid(): - first_party_url = None - else: - first_party_url = info.first_party_url if not config.get('content.host_blocking.enabled', url=first_party_url): - return + return False - host = info.request_url.host() - blocked = ((host in self._blocked_hosts or - host in self._config_blocked_hosts) and - not _is_whitelisted_url(info.request_url)) + host = request_url.host() + return ((host in self._blocked_hosts or + host in self._config_blocked_hosts) and + not _is_whitelisted_url(request_url)) - if blocked: + def filter_request(self, info: requests.Request) -> None: + """Block the given request if necessary.""" + if self._is_blocked(request_url=info.request_url, + first_party_url=info.first_party_url): logger.info("Request to {} blocked by host blocker." .format(info.request_url.host())) info.block() diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index b45427043..f993cbf19 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -44,6 +44,7 @@ from PyQt5.QtNetwork import QNetworkCookieJar import helpers.stubs as stubsmod from qutebrowser.config import (config, configdata, configtypes, configexc, configfiles, configcache) +from qutebrowser.api import config as configapi from qutebrowser.utils import objreg, standarddir, utils, usertypes from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies @@ -306,6 +307,7 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub): container = config.ConfigContainer(conf) monkeypatch.setattr(config, 'val', container) + monkeypatch.setattr(configapi, 'val', container) cache = configcache.ConfigCache() monkeypatch.setattr(config, 'cache', cache) diff --git a/tests/unit/components/test_adblock.py b/tests/unit/components/test_adblock.py index e354705dd..f37b57962 100644 --- a/tests/unit/components/test_adblock.py +++ b/tests/unit/components/test_adblock.py @@ -33,7 +33,7 @@ from qutebrowser.utils import urlmatch from tests.helpers import utils -pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir') +pytestmark = pytest.mark.usefixtures('qapp') # TODO See ../utils/test_standarddirutils for OSError and caplog assertion @@ -58,18 +58,13 @@ URLS_TO_CHECK = ('http://localhost', 'http://veryverygoodhost.edu') -class BaseDirStub: - - """Mock for objreg.get('args') called in adblock.HostBlocker.read_hosts.""" - - def __init__(self): - self.basedir = None - - @pytest.fixture -def basedir(fake_args): - """Register a Fake basedir.""" - fake_args.basedir = None +def host_blocker_factory(config_tmpdir, data_tmpdir, download_stub, + config_stub): + def factory(): + return adblock.HostBlocker(config_dir=config_tmpdir, + data_dir=data_tmpdir) + return factory def create_zipfile(directory, files, zipname='test'): @@ -133,9 +128,9 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS, url = QUrl(str_url) host = url.host() if host in blocked and host not in whitelisted: - assert host_blocker.is_blocked(url) + assert host_blocker._is_blocked(url) else: - assert not host_blocker.is_blocked(url) + assert not host_blocker._is_blocked(url) def blocklist_to_url(filename): @@ -202,13 +197,13 @@ def generic_blocklists(directory): blocklist5.toString()] -def test_disabled_blocking_update(basedir, config_stub, download_stub, - data_tmpdir, tmpdir, win_registry, caplog): +def test_disabled_blocking_update(config_stub, tmpdir, caplog, + host_blocker_factory): """Ensure no URL is blocked when host blocking is disabled.""" config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir) config_stub.val.content.host_blocking.enabled = False - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() while host_blocker._in_progress: current_download = host_blocker._in_progress[0] @@ -217,10 +212,10 @@ def test_disabled_blocking_update(basedir, config_stub, download_stub, current_download.finished.emit() host_blocker.read_hosts() for str_url in URLS_TO_CHECK: - assert not host_blocker.is_blocked(QUrl(str_url)) + assert not host_blocker._is_blocked(QUrl(str_url)) -def test_disabled_blocking_per_url(config_stub, data_tmpdir): +def test_disabled_blocking_per_url(config_stub, host_blocker_factory): example_com = 'https://www.example.com/' config_stub.val.content.host_blocking.lists = [] @@ -230,36 +225,34 @@ def test_disabled_blocking_per_url(config_stub, data_tmpdir): url = QUrl('blocked.example.com') - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker._blocked_hosts.add(url.host()) - assert host_blocker.is_blocked(url) - assert not host_blocker.is_blocked(url, first_party_url=QUrl(example_com)) + assert host_blocker._is_blocked(url) + assert not host_blocker._is_blocked(url, first_party_url=QUrl(example_com)) -def test_no_blocklist_update(config_stub, download_stub, - data_tmpdir, basedir, tmpdir, win_registry): +def test_no_blocklist_update(config_stub, download_stub, host_blocker_factory): """Ensure no URL is blocked when no block list exists.""" config_stub.val.content.host_blocking.lists = None config_stub.val.content.host_blocking.enabled = True - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() host_blocker.read_hosts() for dl in download_stub.downloads: dl.successful = True for str_url in URLS_TO_CHECK: - assert not host_blocker.is_blocked(QUrl(str_url)) + assert not host_blocker._is_blocked(QUrl(str_url)) -def test_successful_update(config_stub, basedir, download_stub, - data_tmpdir, tmpdir, win_registry, caplog): +def test_successful_update(config_stub, tmpdir, caplog, host_blocker_factory): """Ensure hosts from host_blocking.lists are blocked after an update.""" config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir) config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = None - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() # Simulate download is finished while host_blocker._in_progress: @@ -271,11 +264,9 @@ def test_successful_update(config_stub, basedir, download_stub, assert_urls(host_blocker, whitelisted=[]) -def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub, - data_tmpdir, tmpdir, win_registry, - caplog): +def test_parsing_multiple_hosts_on_line(host_blocker_factory): """Ensure multiple hosts on a line get parsed correctly.""" - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8') host_blocker._parse_line(bytes_host_line) assert_urls(host_blocker, whitelisted=[]) @@ -299,17 +290,15 @@ def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub, ('127.0.1.1', 'myhostname'), ('127.0.0.53', 'myhostname'), ]) -def test_whitelisted_lines(config_stub, basedir, download_stub, data_tmpdir, - tmpdir, win_registry, caplog, ip, host): +def test_whitelisted_lines(host_blocker_factory, ip, host): """Make sure we don't block hosts we don't want to.""" - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() line = ('{} {}'.format(ip, host)).encode('ascii') host_blocker._parse_line(line) assert host not in host_blocker._blocked_hosts -def test_failed_dl_update(config_stub, basedir, download_stub, - data_tmpdir, tmpdir, win_registry, caplog): +def test_failed_dl_update(config_stub, tmpdir, caplog, host_blocker_factory): """One blocklist fails to download. Ensure hosts from this list are not blocked. @@ -323,7 +312,7 @@ def test_failed_dl_update(config_stub, basedir, download_stub, config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = None - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() while host_blocker._in_progress: current_download = host_blocker._in_progress[0] @@ -339,8 +328,8 @@ def test_failed_dl_update(config_stub, basedir, download_stub, @pytest.mark.parametrize('location', ['content', 'comment']) -def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir, - caplog, location): +def test_invalid_utf8(config_stub, tmpdir, caplog, host_blocker_factory, + location): """Make sure invalid UTF-8 is handled correctly. See https://github.com/qutebrowser/qutebrowser/issues/2301 @@ -359,7 +348,7 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir, config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = None - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() current_download = host_blocker._in_progress[0] @@ -379,26 +368,25 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir, def test_invalid_utf8_compiled(config_stub, config_tmpdir, data_tmpdir, - monkeypatch, caplog): + monkeypatch, caplog, host_blocker_factory): """Make sure invalid UTF-8 in the compiled file is handled.""" config_stub.val.content.host_blocking.lists = [] # Make sure the HostBlocker doesn't delete blocked-hosts in __init__ - monkeypatch.setattr(adblock.HostBlocker, '_update_files', + monkeypatch.setattr(adblock.HostBlocker, 'update_files', lambda _self: None) (config_tmpdir / 'blocked-hosts').write_binary( b'https://www.example.org/\xa0') (data_tmpdir / 'blocked-hosts').ensure() - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() with caplog.at_level(logging.ERROR): host_blocker.read_hosts() assert caplog.messages[-1] == "Failed to read host blocklist!" -def test_blocking_with_whitelist(config_stub, basedir, download_stub, - data_tmpdir, tmpdir): +def test_blocking_with_whitelist(config_stub, data_tmpdir, host_blocker_factory): """Ensure hosts in content.host_blocking.whitelist are never blocked.""" # Simulate adblock_update has already been run # by creating a file named blocked-hosts, @@ -412,13 +400,12 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub, config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = list(WHITELISTED_HOSTS) - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.read_hosts() assert_urls(host_blocker) -def test_config_change_initial(config_stub, basedir, download_stub, - data_tmpdir, tmpdir): +def test_config_change_initial(config_stub, tmpdir, host_blocker_factory): """Test emptying host_blocking.lists with existing blocked_hosts. - A blocklist is present in host_blocking.lists and blocked_hosts is @@ -432,14 +419,13 @@ def test_config_change_initial(config_stub, basedir, download_stub, config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = None - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.read_hosts() for str_url in URLS_TO_CHECK: - assert not host_blocker.is_blocked(QUrl(str_url)) + assert not host_blocker._is_blocked(QUrl(str_url)) -def test_config_change(config_stub, basedir, download_stub, - data_tmpdir, tmpdir): +def test_config_change(config_stub, tmpdir, host_blocker_factory): """Ensure blocked-hosts resets if host-block-list is changed to None.""" filtered_blocked_hosts = BLOCKLIST_HOSTS[1:] # Exclude localhost blocklist = blocklist_to_url(create_blocklist( @@ -449,16 +435,15 @@ def test_config_change(config_stub, basedir, download_stub, config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.whitelist = None - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.read_hosts() config_stub.val.content.host_blocking.lists = None host_blocker.read_hosts() for str_url in URLS_TO_CHECK: - assert not host_blocker.is_blocked(QUrl(str_url)) + assert not host_blocker._is_blocked(QUrl(str_url)) -def test_add_directory(config_stub, basedir, download_stub, - data_tmpdir, tmpdir): +def test_add_directory(config_stub, tmpdir, host_blocker_factory): """Ensure adblocker can import all files in a directory.""" blocklist_hosts2 = [] for i in BLOCKLIST_HOSTS[1:]: @@ -471,18 +456,18 @@ def test_add_directory(config_stub, basedir, download_stub, config_stub.val.content.host_blocking.lists = [tmpdir.strpath] config_stub.val.content.host_blocking.enabled = True - host_blocker = adblock.HostBlocker() + host_blocker = host_blocker_factory() host_blocker.adblock_update() assert len(host_blocker._blocked_hosts) == len(blocklist_hosts2) * 2 -def test_adblock_benchmark(config_stub, data_tmpdir, basedir, benchmark): +def test_adblock_benchmark(data_tmpdir, benchmark, host_blocker_factory): blocked_hosts = os.path.join(utils.abs_datapath(), 'blocked-hosts') shutil.copy(blocked_hosts, str(data_tmpdir)) url = QUrl('https://www.example.org/') - blocker = adblock.HostBlocker() + blocker = host_blocker_factory() blocker.read_hosts() assert blocker._blocked_hosts - benchmark(lambda: blocker.is_blocked(url)) + benchmark(lambda: blocker._is_blocked(url)) From 7f06b54f25200f8af38dec5acc2510f255912ee4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 17:22:07 +0100 Subject: [PATCH 035/117] Add more extensions.loader tests --- qutebrowser/extensions/loader.py | 3 +- tests/unit/extensions/test_loader.py | 97 +++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 1a9b3d858..d7a106344 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -142,7 +142,8 @@ def _get_init_context() -> InitContext: args=objreg.get('args')) -def _load_component(info: ExtensionInfo, *, skip_hooks) -> types.ModuleType: +def _load_component(info: ExtensionInfo, *, + skip_hooks=False) -> types.ModuleType: """Load the given extension and run its init hook (if any). Args: diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py index 710b1ce9c..0a7edc9d3 100644 --- a/tests/unit/extensions/test_loader.py +++ b/tests/unit/extensions/test_loader.py @@ -17,12 +17,18 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import types + import pytest from qutebrowser.extensions import loader from qutebrowser.misc import objects +pytestmark = pytest.mark.usefixtures('data_tmpdir', 'config_tmpdir', + 'fake_args') + + def test_on_walk_error(): with pytest.raises(ImportError, match='Failed to import foo'): loader._on_walk_error('foo') @@ -43,7 +49,94 @@ def test_load_component(monkeypatch): monkeypatch.setattr(objects, 'commands', {}) info = loader.ExtensionInfo(name='qutebrowser.components.scrollcommands') - module = loader._load_component(info, skip_hooks=True) + mod = loader._load_component(info, skip_hooks=True) - assert hasattr(module, 'scroll_to_perc') + assert hasattr(mod, 'scroll_to_perc') assert 'scroll-to-perc' in objects.commands + + +@pytest.fixture +def module(monkeypatch, request): + mod = types.ModuleType('testmodule') + + monkeypatch.setattr(loader, '_module_infos', []) + monkeypatch.setattr(loader.importlib, 'import_module', + lambda _name: mod) + + mod.info = loader.add_module_info(mod) + return mod + + +def test_get_init_context(data_tmpdir, config_tmpdir, fake_args): + ctx = loader._get_init_context() + assert str(ctx.data_dir) == data_tmpdir + assert str(ctx.config_dir) == config_tmpdir + assert ctx.args == fake_args + + +def test_add_module_info(): + mod = types.ModuleType('testmodule') + info1 = loader.add_module_info(mod) + assert mod.__qute_module_info is info1 + + info2 = loader.add_module_info(mod) + assert mod.__qute_module_info is info1 + assert info2 is info1 + + +class _Hook: + + """Hook to use in tests.""" + + __name__ = '_Hook' + + def __init__(self): + self.called = False + self.raising = False + + def __call__(self, *args): + if self.raising: + raise Exception("Should not be called!") + self.called = True + + +@pytest.fixture +def hook(): + return _Hook() + + +def test_skip_hooks(hook, module): + hook.raising = True + + module.info.init_hook = hook + module.info.config_changed_hooks = [(None, hook)] + + info = loader.ExtensionInfo(name='testmodule') + loader._load_component(info, skip_hooks=True) + loader._on_config_changed('test') + + assert not hook.called + + +@pytest.mark.parametrize('option_filter, option, called', [ + (None, 'content.javascript.enabled', True), + ('content.javascript', 'content.javascript.enabled', True), + ('content.javascript.enabled', 'content.javascript.enabled', True), + ('content.javascript.log', 'content.javascript.enabled', False), +]) +def test_on_config_changed(configdata_init, hook, module, + option_filter, option, called): + module.info.config_changed_hooks = [(option_filter, hook)] + + info = loader.ExtensionInfo(name='testmodule') + loader._load_component(info) + loader._on_config_changed(option) + + assert hook.called == called + + +def test_init_hook(hook, module): + module.info.init_hook = hook + info = loader.ExtensionInfo(name='testmodule') + loader._load_component(info) + assert hook.called From 98543af57b23d57f11918c51b0c1d1c863a223b3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 17:35:53 +0100 Subject: [PATCH 036/117] Rename requests/request filters to interceptors So we don't collide with the requests library. --- .../api/{requests.py => interceptor.py} | 12 ++++++------ qutebrowser/browser/webengine/interceptor.py | 8 ++++---- qutebrowser/browser/webkit/mhtml.py | 6 +++--- .../browser/webkit/network/networkmanager.py | 8 ++++---- qutebrowser/components/adblock.py | 6 +++--- .../{requests.py => interceptors.py} | 18 +++++++++--------- 6 files changed, 29 insertions(+), 29 deletions(-) rename qutebrowser/api/{requests.py => interceptor.py} (72%) rename qutebrowser/extensions/{requests.py => interceptors.py} (74%) diff --git a/qutebrowser/api/requests.py b/qutebrowser/api/interceptor.py similarity index 72% rename from qutebrowser/api/requests.py rename to qutebrowser/api/interceptor.py index 5c23418b4..afd23d067 100644 --- a/qutebrowser/api/requests.py +++ b/qutebrowser/api/interceptor.py @@ -19,14 +19,14 @@ """APIs related to intercepting/blocking requests.""" -from qutebrowser.extensions import requests +from qutebrowser.extensions import interceptors # pylint: disable=unused-import -from qutebrowser.extensions.requests import Request +from qutebrowser.extensions.interceptors import Request -def register_filter(reqfilter: requests.RequestFilterType) -> None: - """Register a request filter. +def register(interceptor: interceptors.InterceptorType) -> None: + """Register a request interceptor. - Whenever a request happens, the filter gets called with a Request object. + Whenever a request happens, the interceptor gets called with a Request object. """ - requests.register_filter(reqfilter) + interceptors.register(interceptor) diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 863234dc6..a5b7721b8 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -26,7 +26,7 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, from qutebrowser.config import config from qutebrowser.browser import shared from qutebrowser.utils import utils, log, debug -from qutebrowser.extensions import requests +from qutebrowser.extensions import interceptors class RequestInterceptor(QWebEngineUrlRequestInterceptor): @@ -84,9 +84,9 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): return # FIXME:qtwebengine only block ads for NavigationTypeOther? - request = requests.Request(first_party_url=first_party, - request_url=url) - requests.run_filters(request) + request = interceptors.Request(first_party_url=first_party, + request_url=url) + interceptors.run(request) if request.is_blocked: info.block(True) diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index c390ab6b3..70a22351f 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -39,7 +39,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.browser import downloads from qutebrowser.browser.webkit import webkitelem from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils -from qutebrowser.extensions import requests +from qutebrowser.extensions import interceptors @attr.s @@ -355,8 +355,8 @@ class _Downloader: # qute, see the comments/discussion on # https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987 # and https://github.com/qutebrowser/qutebrowser/issues/1053 - request = requests.Request(first_party_url=None, request_url=url) - requests.run_filters(request) + request = interceptors.Request(first_party_url=None, request_url=url) + interceptors.run(request) if request.is_blocked: log.downloads.debug("Skipping {}, host-blocked".format(url)) # We still need an empty file in the output, QWebView can be pretty diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 9562ba918..dd3643c87 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -38,7 +38,7 @@ if MYPY: from qutebrowser.utils import (message, log, usertypes, utils, objreg, urlutils, debug) from qutebrowser.browser import shared -from qutebrowser.extensions import requests +from qutebrowser.extensions import interceptors from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, filescheme) @@ -406,9 +406,9 @@ class NetworkManager(QNetworkAccessManager): # the webpage shutdown here. current_url = QUrl() - request = requests.Request(first_party_url=current_url, - request_url=req.url()) - requests.run_filters(request) + request = interceptors.Request(first_party_url=current_url, + request_url=req.url()) + interceptors.run(request) if request.is_blocked: return networkreply.ErrorNetworkReply( req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied, diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index d279f1c03..bccb53532 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -31,7 +31,7 @@ import pathlib from PyQt5.QtCore import QUrl from qutebrowser.api import (cmdutils, hook, config, message, downloads, - requests, apitypes) + interceptor, apitypes) logger = logging.getLogger('misc') @@ -123,7 +123,7 @@ class HostBlocker: host in self._config_blocked_hosts) and not _is_whitelisted_url(request_url)) - def filter_request(self, info: requests.Request) -> None: + def filter_request(self, info: interceptor.Request) -> None: """Block the given request if necessary.""" if self._is_blocked(request_url=info.request_url, first_party_url=info.first_party_url): @@ -344,4 +344,4 @@ def init(context: apitypes.InitContext) -> None: config_dir=context.config_dir, has_basedir=context.args.basedir is not None) _host_blocker.read_hosts() - requests.register_filter(_host_blocker.filter_request) + interceptor.register(_host_blocker.filter_request) diff --git a/qutebrowser/extensions/requests.py b/qutebrowser/extensions/interceptors.py similarity index 74% rename from qutebrowser/extensions/requests.py rename to qutebrowser/extensions/interceptors.py index 952c830dc..4a3ac17d4 100644 --- a/qutebrowser/extensions/requests.py +++ b/qutebrowser/extensions/interceptors.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Infrastructure for filtering requests.""" +"""Infrastructure for intercepting requests.""" import typing @@ -32,7 +32,7 @@ if MYPY: @attr.s class Request: - """A request which can be blocked.""" + """A request which can be intercepted/blocked.""" first_party_url = attr.ib() # type: QUrl request_url = attr.ib() # type: QUrl @@ -43,16 +43,16 @@ class Request: self.is_blocked = True -RequestFilterType = typing.Callable[[Request], None] +InterceptorType = typing.Callable[[Request], None] -_request_filters = [] # type: typing.List[RequestFilterType] +_interceptors = [] # type: typing.List[InterceptorType] -def register_filter(reqfilter: RequestFilterType) -> None: - _request_filters.append(reqfilter) +def register(interceptor: InterceptorType) -> None: + _interceptors.append(interceptor) -def run_filters(info: Request) -> None: - for reqfilter in _request_filters: - reqfilter(info) +def run(info: Request) -> None: + for interceptor in _interceptors: + interceptor(info) From a3279772d56524f239a82674281618e087499879 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 18:34:11 +0100 Subject: [PATCH 037/117] Fix lint --- qutebrowser/api/interceptor.py | 3 ++- qutebrowser/components/adblock.py | 2 +- qutebrowser/extensions/loader.py | 4 ++-- tests/unit/extensions/test_loader.py | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/api/interceptor.py b/qutebrowser/api/interceptor.py index afd23d067..a40635fca 100644 --- a/qutebrowser/api/interceptor.py +++ b/qutebrowser/api/interceptor.py @@ -27,6 +27,7 @@ from qutebrowser.extensions.interceptors import Request def register(interceptor: interceptors.InterceptorType) -> None: """Register a request interceptor. - Whenever a request happens, the interceptor gets called with a Request object. + Whenever a request happens, the interceptor gets called with a Request + object. """ interceptors.register(interceptor) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index bccb53532..9baa12d7c 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -109,7 +109,7 @@ class HostBlocker: self._config_hosts_file = str(config_dir / 'blocked-hosts') def _is_blocked(self, request_url: QUrl, - first_party_url: QUrl = None) -> None: + first_party_url: QUrl = None) -> bool: """Check whether the given request is blocked.""" if first_party_url is not None and not first_party_url.isValid(): first_party_url = None diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index d7a106344..1383adfef 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -87,7 +87,7 @@ def add_module_info(module: types.ModuleType) -> ModuleInfo: return module.__qute_module_info # type: ignore -def load_components(*, skip_hooks=False) -> None: +def load_components(*, skip_hooks: bool = False) -> None: """Load everything from qutebrowser.components.""" for info in walk_components(): _load_component(info, skip_hooks=skip_hooks) @@ -143,7 +143,7 @@ def _get_init_context() -> InitContext: def _load_component(info: ExtensionInfo, *, - skip_hooks=False) -> types.ModuleType: + skip_hooks: bool = False) -> types.ModuleType: """Load the given extension and run its init hook (if any). Args: diff --git a/tests/unit/extensions/test_loader.py b/tests/unit/extensions/test_loader.py index 0a7edc9d3..5265c7cdf 100644 --- a/tests/unit/extensions/test_loader.py +++ b/tests/unit/extensions/test_loader.py @@ -75,6 +75,7 @@ def test_get_init_context(data_tmpdir, config_tmpdir, fake_args): def test_add_module_info(): + # pylint: disable=no-member mod = types.ModuleType('testmodule') info1 = loader.add_module_info(mod) assert mod.__qute_module_info is info1 From 81930d73a81504eddbb76349dd9e846ba079d909 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:13 +0100 Subject: [PATCH 038/117] Update idna from 2.7 to 2.8 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 4ce97a976..c42e9991b 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -4,6 +4,6 @@ certifi==2018.11.29 chardet==3.0.4 codecov==2.0.15 coverage==4.5.2 -idna==2.7 +idna==2.8 requests==2.20.1 urllib3==1.24.1 From d4c49718213d6e5d8705de7bd3c77ef81de04798 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:14 +0100 Subject: [PATCH 039/117] Update idna from 2.7 to 2.8 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 39e731984..644099a96 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -7,7 +7,7 @@ cffi==1.11.5 chardet==3.0.4 cryptography==2.4.2 github3.py==1.2.0 -idna==2.7 +idna==2.8 isort==4.3.4 jwcrypto==0.6.0 lazy-object-proxy==1.3.1 From 7c72c7565c744a26803d451b72049dd89d154268 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:16 +0100 Subject: [PATCH 040/117] Update requests from 2.20.1 to 2.21.0 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index c42e9991b..19ae758af 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -5,5 +5,5 @@ chardet==3.0.4 codecov==2.0.15 coverage==4.5.2 idna==2.8 -requests==2.20.1 +requests==2.21.0 urllib3==1.24.1 From a12fd6721818f74aa3a1190ed8e17b4c809c28a0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:18 +0100 Subject: [PATCH 041/117] Update requests from 2.20.1 to 2.21.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 644099a96..97eaf3fdf 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -16,7 +16,7 @@ pycparser==2.19 pylint==2.2.2 python-dateutil==2.7.5 ./scripts/dev/pylint_checkers -requests==2.20.1 +requests==2.21.0 six==1.11.0 uritemplate==3.0.0 urllib3==1.24.1 From 4288b71039ba74a7be4cd50b3633ee9db1ff0eca Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:19 +0100 Subject: [PATCH 042/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index cd1fcba4d..20a66cd5f 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -22,6 +22,6 @@ pep8-naming==0.7.0 pycodestyle==2.4.0 pydocstyle==3.0.0 pyflakes==2.0.0 -six==1.11.0 +six==1.12.0 snowballstemmer==1.2.1 typing==3.6.6 From aab6ba9ef1582c676933f631408d7af68ff085bc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:21 +0100 Subject: [PATCH 043/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-optional.txt b/misc/requirements/requirements-optional.txt index 8f1e2498a..aafa38e46 100644 --- a/misc/requirements/requirements-optional.txt +++ b/misc/requirements/requirements-optional.txt @@ -4,4 +4,4 @@ colorama==0.4.1 cssutils==1.0.2 hunter==2.1.0 Pympler==0.6 -six==1.11.0 +six==1.12.0 From 9b6b0ef1a52b8ccb554215f1e4ae76ea5edf3f4d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:22 +0100 Subject: [PATCH 044/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 43085ddd7..1e1934e76 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -4,5 +4,5 @@ appdirs==1.4.3 packaging==18.0 pyparsing==2.3.0 setuptools==40.6.2 -six==1.11.0 +six==1.12.0 wheel==0.32.3 From fe919769ba3b3ef85a560e37db503871469d864f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:24 +0100 Subject: [PATCH 045/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 97eaf3fdf..b3ecdaf70 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -17,7 +17,7 @@ pylint==2.2.2 python-dateutil==2.7.5 ./scripts/dev/pylint_checkers requests==2.21.0 -six==1.11.0 +six==1.12.0 uritemplate==3.0.0 urllib3==1.24.1 wrapt==1.10.11 From f8c56ffaf6336102b369b04fd816aceb3ada7d62 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:25 +0100 Subject: [PATCH 046/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 7d8a4f33e..24dc51613 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -36,6 +36,6 @@ pytest-rerunfailures==5.0 pytest-travis-fold==1.3.0 pytest-xvfb==1.1.0 PyVirtualDisplay==0.2.1 -six==1.11.0 +six==1.12.0 vulture==1.0 Werkzeug==0.14.1 From 9c141314c53bfde9887ca1f55bc8f911a01ce4f6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:27 +0100 Subject: [PATCH 047/117] Update six from 1.11.0 to 1.12.0 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 097859b6e..b6c9e9d6f 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -3,7 +3,7 @@ filelock==3.0.10 pluggy==0.8.0 py==1.7.0 -six==1.11.0 +six==1.12.0 toml==0.10.0 tox==3.5.3 virtualenv==16.1.0 From db62c52c442147cdf99c3b192318c37b73206089 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:28 +0100 Subject: [PATCH 048/117] Update mypy from 0.641 to 0.650 --- misc/requirements/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index f2951fdf5..f84b63e92 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -mypy==0.641 +mypy==0.650 mypy-extensions==0.4.1 PyQt5==5.11.3 PyQt5-sip==4.19.13 From 20e9c22dc63af3e147496e1aeda9d44fd0c13f84 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Dec 2018 19:19:30 +0100 Subject: [PATCH 049/117] Update hypothesis from 3.82.1 to 3.82.5 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 24dc51613..9b4bcc96f 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -12,7 +12,7 @@ EasyProcess==0.2.3 Flask==1.0.2 glob2==0.6 hunter==2.1.0 -hypothesis==3.82.1 +hypothesis==3.82.5 itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 From 4a1333795f5daccbcdd198db2478f538db156048 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 19:52:20 +0100 Subject: [PATCH 050/117] Add api/interceptor.py to check_coverage --- scripts/dev/check_coverage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 62c0b5142..d42ce1d71 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -204,6 +204,7 @@ WHITELISTED_FILES = [ 'browser/webkit/webkitinspector.py', 'keyinput/macros.py', 'browser/webkit/webkitelem.py', + 'api/interceptor.py', ] From 184e41b5389d5c1e8c5cbcf873cd55eefa84c4d7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 20:03:31 +0100 Subject: [PATCH 051/117] Adjustments for mypy upgrade --- mypy.ini | 4 ---- qutebrowser/utils/utils.py | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mypy.ini b/mypy.ini index 8fb8d89ae..b8b2cf16f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,10 +18,6 @@ disallow_untyped_decorators = True # no_implicit_optional = True # warn_return_any = True -[mypy-faulthandler] -# https://github.com/python/typeshed/pull/2627 -ignore_missing_imports = True - [mypy-colorama] # https://github.com/tartley/colorama/issues/206 ignore_missing_imports = True diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 8574a3c2b..2d517043a 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -45,7 +45,8 @@ try: CSafeDumper as YamlDumper) YAML_C_EXT = True except ImportError: # pragma: no cover - from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper + from yaml import (SafeLoader as YamlLoader, # type: ignore + SafeDumper as YamlDumper) YAML_C_EXT = False import qutebrowser From 0aa342eae617f5c6722fdf7b14d78d8f58cc0849 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 11:05:06 +0100 Subject: [PATCH 052/117] Mark test_appendchild as xfail on Qt 5.12 See #4244 --- tests/unit/javascript/stylesheet/test_stylesheet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/javascript/stylesheet/test_stylesheet.py b/tests/unit/javascript/stylesheet/test_stylesheet.py index 2b1c7ced5..c58a0401d 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet.py @@ -25,7 +25,7 @@ import pytest QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets") QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile -from qutebrowser.utils import javascript +from qutebrowser.utils import javascript, qtutils DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)" @@ -128,6 +128,8 @@ def test_set_error(stylesheet_tester, config_stub): stylesheet_tester.check_set(GREEN_BODY_BG) +@pytest.mark.xfail(qtutils.version_check('5.12', compiled=False), + reason='Broken with Qt 5.12') def test_appendchild(stylesheet_tester): stylesheet_tester.js.load('stylesheet/simple.html') stylesheet_tester.init_stylesheet() From 63510c41f5b8da4cad35133ba63afb313fe5d731 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 18:36:20 +0100 Subject: [PATCH 053/117] Don't wait for focus ready message on Qt 5.12 Looks like there's no internal widget focused anymore... --- tests/end2end/fixtures/quteprocess.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 561d92a80..146817c12 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -356,7 +356,10 @@ class QuteProc(testprocess.Process): self._focus_ready = True else: raise ValueError("Invalid value {!r} for 'what'.".format(what)) - if self._load_ready and self._focus_ready: + + is_qt_5_12 = qtutils.version_check('5.12', compiled=False) + if ((self._load_ready and self._focus_ready) or + (self._load_ready and is_qt_5_12)): self._load_ready = False self._focus_ready = False self.ready.emit() From 0953596ed6761f197dcd5f137175f5c57fe37eeb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 18:36:52 +0100 Subject: [PATCH 054/117] Skip remaining tests which are broken on Qt 5.12 See #4320, #4244 --- tests/end2end/features/javascript.feature | 6 +++--- tests/unit/javascript/stylesheet/test_stylesheet.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index e4b477e50..8fedf5af1 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -124,9 +124,9 @@ Feature: Javascript stuff # https://github.com/qutebrowser/qutebrowser/issues/1190 # https://github.com/qutebrowser/qutebrowser/issues/2495 - # Currently broken on Windows: + # Currently broken on Windows and on Qt 5.12 # https://github.com/qutebrowser/qutebrowser/issues/4230 - @posix + @posix @qt<5.12 Scenario: Checking visible/invisible window size When I run :tab-only And I open data/javascript/windowsize.html in a new background tab @@ -134,7 +134,7 @@ Feature: Javascript stuff And I run :tab-next Then the window sizes should be the same - @flaky + @flaky @qt<5.12 Scenario: Checking visible/invisible window size with vertical tabbar When I run :tab-only And I set tabs.position to left diff --git a/tests/unit/javascript/stylesheet/test_stylesheet.py b/tests/unit/javascript/stylesheet/test_stylesheet.py index c58a0401d..145e8ee5e 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet.py @@ -128,8 +128,8 @@ def test_set_error(stylesheet_tester, config_stub): stylesheet_tester.check_set(GREEN_BODY_BG) -@pytest.mark.xfail(qtutils.version_check('5.12', compiled=False), - reason='Broken with Qt 5.12') +@pytest.mark.skip(qtutils.version_check('5.12', compiled=False), + reason='Broken with Qt 5.12') def test_appendchild(stylesheet_tester): stylesheet_tester.js.load('stylesheet/simple.html') stylesheet_tester.init_stylesheet() From 0fbe4371a7b64e0ba39e379a98e97573d28093e9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 19:02:04 +0100 Subject: [PATCH 055/117] Initialize Sphinx --- .gitignore | 1 + doc/extapi/Makefile | 19 ++++ doc/extapi/_static/.gitkeep | 0 doc/extapi/_templates/.gitkeep | 0 doc/extapi/conf.py | 177 +++++++++++++++++++++++++++++++++ doc/extapi/index.rst | 20 ++++ doc/extapi/make.bat | 35 +++++++ 7 files changed, 252 insertions(+) create mode 100644 doc/extapi/Makefile create mode 100644 doc/extapi/_static/.gitkeep create mode 100644 doc/extapi/_templates/.gitkeep create mode 100644 doc/extapi/conf.py create mode 100644 doc/extapi/index.rst create mode 100644 doc/extapi/make.bat diff --git a/.gitignore b/.gitignore index 9efceef63..ceafd9946 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ TODO /scripts/testbrowser/cpp/webengine/.qmake.stash /scripts/dev/pylint_checkers/qute_pylint.egg-info /misc/file_version_info.txt +/doc/extapi/_build diff --git a/doc/extapi/Makefile b/doc/extapi/Makefile new file mode 100644 index 000000000..298ea9e21 --- /dev/null +++ b/doc/extapi/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/doc/extapi/_static/.gitkeep b/doc/extapi/_static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/doc/extapi/_templates/.gitkeep b/doc/extapi/_templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/doc/extapi/conf.py b/doc/extapi/conf.py new file mode 100644 index 000000000..986290a48 --- /dev/null +++ b/doc/extapi/conf.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'qutebrowser extensions' +copyright = '2018, Florian Bruhin' +author = 'Florian Bruhin' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'qutebrowserextensionsdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'qutebrowserextensions.tex', 'qutebrowser extensions Documentation', + 'Florian Bruhin', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation', + author, 'qutebrowserextensions', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- \ No newline at end of file diff --git a/doc/extapi/index.rst b/doc/extapi/index.rst new file mode 100644 index 000000000..896c43891 --- /dev/null +++ b/doc/extapi/index.rst @@ -0,0 +1,20 @@ +.. qutebrowser extensions documentation master file, created by + sphinx-quickstart on Tue Dec 11 18:59:44 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to qutebrowser extensions's documentation! +================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/extapi/make.bat b/doc/extapi/make.bat new file mode 100644 index 000000000..27f573b87 --- /dev/null +++ b/doc/extapi/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From 24d45bfcba46dabceb3964d0eebc491873a44f7b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 19:08:21 +0100 Subject: [PATCH 056/117] Move Sphinx to tox --- doc/extapi/Makefile | 19 ---------- doc/extapi/make.bat | 35 ------------------- misc/requirements/requirements-sphinx.txt | 21 +++++++++++ misc/requirements/requirements-sphinx.txt-raw | 1 + tox.ini | 10 ++++++ 5 files changed, 32 insertions(+), 54 deletions(-) delete mode 100644 doc/extapi/Makefile delete mode 100644 doc/extapi/make.bat create mode 100644 misc/requirements/requirements-sphinx.txt create mode 100644 misc/requirements/requirements-sphinx.txt-raw diff --git a/doc/extapi/Makefile b/doc/extapi/Makefile deleted file mode 100644 index 298ea9e21..000000000 --- a/doc/extapi/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/doc/extapi/make.bat b/doc/extapi/make.bat deleted file mode 100644 index 27f573b87..000000000 --- a/doc/extapi/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt new file mode 100644 index 000000000..c0801a48e --- /dev/null +++ b/misc/requirements/requirements-sphinx.txt @@ -0,0 +1,21 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +alabaster==0.7.12 +Babel==2.6.0 +certifi==2018.11.29 +chardet==3.0.4 +docutils==0.14 +idna==2.8 +imagesize==1.1.0 +Jinja2==2.10 +MarkupSafe==1.1.0 +packaging==18.0 +Pygments==2.3.0 +pyparsing==2.3.0 +pytz==2018.7 +requests==2.21.0 +six==1.12.0 +snowballstemmer==1.2.1 +Sphinx==1.8.2 +sphinxcontrib-websupport==1.1.0 +urllib3==1.24.1 diff --git a/misc/requirements/requirements-sphinx.txt-raw b/misc/requirements/requirements-sphinx.txt-raw new file mode 100644 index 000000000..6966869c7 --- /dev/null +++ b/misc/requirements/requirements-sphinx.txt-raw @@ -0,0 +1 @@ +sphinx diff --git a/tox.ini b/tox.ini index 8a4232aaa..8fb312798 100644 --- a/tox.ini +++ b/tox.ini @@ -199,3 +199,13 @@ deps = -r{toxinidir}/misc/requirements/requirements-mypy.txt commands = {envpython} -m mypy qutebrowser {posargs} + +[testenv:sphinx] +basepython = {env:PYTHON:python3} +passenv = +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-pyqt.txt + -r{toxinidir}/misc/requirements/requirements-sphinx.txt +commands = + {envpython} -m sphinx doc/extapi/ doc/extapi/_build/ From 61bfecc4abf367180ad3387ea6bd06318fe67767 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Dec 2018 19:20:46 +0100 Subject: [PATCH 057/117] Initial sphinx docs --- doc/extapi/conf.py | 3 ++- doc/extapi/index.rst | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/doc/extapi/conf.py b/doc/extapi/conf.py index 986290a48..09105248a 100644 --- a/doc/extapi/conf.py +++ b/doc/extapi/conf.py @@ -40,6 +40,7 @@ release = '' # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', ] # Add any paths that contain templates here, relative to this directory. @@ -174,4 +175,4 @@ epub_title = project epub_exclude_files = ['search.html'] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- diff --git a/doc/extapi/index.rst b/doc/extapi/index.rst index 896c43891..e176e3a0d 100644 --- a/doc/extapi/index.rst +++ b/doc/extapi/index.rst @@ -10,7 +10,53 @@ Welcome to qutebrowser extensions's documentation! :maxdepth: 2 :caption: Contents: +cmdutils module +--------------- +.. automodule:: qutebrowser.api.cmdutils + :members: + +.. FIXME: What about imported things? + +apitypes module +--------------- + +.. automodule:: qutebrowser.api.apitypes + :members: + :imported-members: + +config module +------------- + +.. automodule:: qutebrowser.api.config + :members: + +downloads module +---------------- + +.. automodule:: qutebrowser.api.downloads + :members: + +hook module +----------- + +.. automodule:: qutebrowser.api.hook + :members: + +interceptor module +------------------ + +.. automodule:: qutebrowser.api.interceptor + :members: + +.. FIXME: What about imported things? + +message module +-------------- + +.. automodule:: qutebrowser.api.message + :members: + :imported-members: Indices and tables ================== From a0437a3e683fa81e271be88c907e25143d0e467c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 08:10:28 +0100 Subject: [PATCH 058/117] Improve Sphinx docs --- doc/extapi/api.rst | 48 +++++++++++ doc/extapi/index.rst | 48 +---------- doc/extapi/tab.rst | 44 +++++++++++ qutebrowser/api/cmdutils.py | 97 ++++++++++++++++++----- qutebrowser/api/config.py | 10 +++ qutebrowser/api/downloads.py | 15 ++++ qutebrowser/api/hook.py | 37 ++++++++- qutebrowser/api/interceptor.py | 10 ++- qutebrowser/browser/browsertab.py | 105 +++++++++++-------------- qutebrowser/browser/webelem.py | 13 +-- qutebrowser/extensions/interceptors.py | 4 + qutebrowser/utils/message.py | 20 ++--- qutebrowser/utils/usertypes.py | 42 +++++++--- 13 files changed, 337 insertions(+), 156 deletions(-) create mode 100644 doc/extapi/api.rst create mode 100644 doc/extapi/tab.rst diff --git a/doc/extapi/api.rst b/doc/extapi/api.rst new file mode 100644 index 000000000..b63db57c3 --- /dev/null +++ b/doc/extapi/api.rst @@ -0,0 +1,48 @@ +API modules +=========== + +cmdutils module +--------------- + +.. automodule:: qutebrowser.api.cmdutils + :members: + :imported-members: + +apitypes module +--------------- + +.. automodule:: qutebrowser.api.apitypes + :members: + :imported-members: + +config module +------------- + +.. automodule:: qutebrowser.api.config + :members: + +downloads module +---------------- + +.. automodule:: qutebrowser.api.downloads + :members: + +hook module +----------- + +.. automodule:: qutebrowser.api.hook + :members: + +interceptor module +------------------ + +.. automodule:: qutebrowser.api.interceptor + :members: + :imported-members: + +message module +-------------- + +.. automodule:: qutebrowser.api.message + :members: + :imported-members: diff --git a/doc/extapi/index.rst b/doc/extapi/index.rst index e176e3a0d..d181c2ccd 100644 --- a/doc/extapi/index.rst +++ b/doc/extapi/index.rst @@ -10,53 +10,9 @@ Welcome to qutebrowser extensions's documentation! :maxdepth: 2 :caption: Contents: -cmdutils module ---------------- + api + tab -.. automodule:: qutebrowser.api.cmdutils - :members: - -.. FIXME: What about imported things? - -apitypes module ---------------- - -.. automodule:: qutebrowser.api.apitypes - :members: - :imported-members: - -config module -------------- - -.. automodule:: qutebrowser.api.config - :members: - -downloads module ----------------- - -.. automodule:: qutebrowser.api.downloads - :members: - -hook module ------------ - -.. automodule:: qutebrowser.api.hook - :members: - -interceptor module ------------------- - -.. automodule:: qutebrowser.api.interceptor - :members: - -.. FIXME: What about imported things? - -message module --------------- - -.. automodule:: qutebrowser.api.message - :members: - :imported-members: Indices and tables ================== diff --git a/doc/extapi/tab.rst b/doc/extapi/tab.rst new file mode 100644 index 000000000..57a14ac6e --- /dev/null +++ b/doc/extapi/tab.rst @@ -0,0 +1,44 @@ +Tab API +======= + +.. autoclass:: qutebrowser.browser.browsertab.AbstractTab() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractAction() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractPrinting() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractSearch() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractZoom() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractCaret() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractScroller() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractHistory() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractElements() + :members: + +.. autoclass:: qutebrowser.browser.browsertab.AbstractAudio() + :members: + +Web element API +=============== + +.. autoclass:: qutebrowser.browser.webelem.AbstractWebElement + :members: + +.. autoclass:: qutebrowser.browser.webelem.Error + :members: + +.. autoclass:: qutebrowser.browser.webelem.OrphanedError + :members: diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 093244727..f6a6f6da9 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -17,7 +17,38 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Utilities for command handlers.""" +""" +qutebrowser has the concept of functions which are exposed to the user as +commands. + +Creating a new command is straightforward:: + + from qutebrowser.api import cmdutils + + @cmdutils.register(...) + def foo(): + ... + +The commands arguments are automatically deduced by inspecting your function. + +The types of the function arguments are inferred based on their default values, +e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in +qutebrowser's commandline. + +The type can be overridden using Python's function annotations:: + + @cmdutils.register(...) + def foo(bar: int, baz=True): + ... + +Possible values: + +- A callable (``int``, ``float``, etc.): Gets called to validate/convert the value. +- A python enum type: All members of the enum are possible values. +- A ``typing.Union`` of multiple types above: Any of these types are valid + values, e.g., ``typing.Union[str, int]``. +""" + import inspect import typing @@ -33,15 +64,17 @@ class CommandError(cmdexc.Error): """Raised when a command encounters an error while running. If your command handler encounters an error and cannot continue, raise this - exception with an appropriate error message: + exception with an appropriate error message:: raise cmdexc.CommandError("Message") The message will then be shown in the qutebrowser status bar. - Note that you should only raise this exception while a command handler is - run. Raising it at another point causes qutebrowser to crash due to an - unhandled exception. + .. note:: + + You should only raise this exception while a command handler is run. + Raising it at another point causes qutebrowser to crash due to an + unhandled exception. """ @@ -76,13 +109,7 @@ def check_exclusive(flags: typing.Iterable[bool], class register: # noqa: N801,N806 pylint: disable=invalid-name - """Decorator to register a new command handler. - - Attributes: - _instance: The object from the object registry to be used as "self". - _name: The name (as string) or names (as list) of the command. - _kwargs: The arguments to pass to Command. - """ + """Decorator to register a new command handler.""" def __init__(self, *, instance: str = None, @@ -95,8 +122,11 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name Args: See class attributes. """ + # The object from the object registry to be used as "self". self._instance = instance + # The name (as string) or names (as list) of the command. self._name = name + # The arguments to pass to Command. self._kwargs = kwargs def __call__(self, func: typing.Callable) -> typing.Callable: @@ -127,16 +157,47 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name class argument: # noqa: N801,N806 pylint: disable=invalid-name - """Decorator to customize an argument for @cmdutils.register. + """Decorator to customize an argument. - Attributes: - _argname: The name of the argument to handle. - _kwargs: Keyword arguments, valid ArgInfo members + You can customize how an argument is handled using the ``@cmdutils.argument`` + decorator *after* ``@cmdutils.register``. This can, for example, be used to + customize the flag an argument should get:: + + @cmdutils.register(...) + @cmdutils.argument('bar', flag='c') + def foo(bar): + ... + + For a ``str`` argument, you can restrict the allowed strings using ``choices``:: + + @cmdutils.register(...) + @cmdutils.argument('bar', choices=['val1', 'val2']) + def foo(bar: str): + ... + + For ``typing.Union`` types, the given ``choices`` are only checked if other types + (like ``int``) don't match. + + The following arguments are supported for ``@cmdutils.argument``: + + - ``flag``: Customize the short flag (``-x``) the argument will get. + - ``value``: Tell qutebrowser to fill the argument with special values: + + * ``value=cmdutils.Value.count``: The ``count`` given by the user to the command. + * ``value=cmdutils.Value.win_id``: The window ID of the current window. + * ``value=cmdutils.Value.cur_tab``: The tab object which is currently focused. + + - ``completion``: A completion function to use when completing arguments for + the given command. + - ``choices``: The allowed string choices for the argument. + + The name of an argument will always be the parameter name, with any trailing + underscores stripped and underscores replaced by dashes. """ def __init__(self, argname: str, **kwargs: typing.Any) -> None: - self._argname = argname - self._kwargs = kwargs + self._argname = argname # The name of the argument to handle. + self._kwargs = kwargs # Valid ArgInfo members. def __call__(self, func: typing.Callable) -> typing.Callable: funcname = func.__name__ diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index 4a5d73936..0c633e54d 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -25,6 +25,16 @@ from PyQt5.QtCore import QUrl from qutebrowser.config import config +#: Simplified access to config values using attribute acccess. +#: For example, to access the ``content.javascript.enabled`` setting, +#: you can do:: +#: +#: if config.val.content.javascript.enabled: +#: ... +#: +#: This also supports setting configuration values:: +#: +#: config.val.content.javascript.enabled = False val = typing.cast('config.ConfigContainer', None) diff --git a/qutebrowser/api/downloads.py b/qutebrowser/api/downloads.py index 82c68d0bd..a2a37d931 100644 --- a/qutebrowser/api/downloads.py +++ b/qutebrowser/api/downloads.py @@ -52,6 +52,21 @@ def download_temp(url: QUrl) -> TempDownload: """Download the given URL into a file object. The download is not saved to disk. + + Returns a ``TempDownload`` object, which triggers a ``finished`` signal + when the download has finished:: + + dl = downloads.download_temp(QUrl("https://www.example.com/")) + dl.finished.connect(functools.partial(on_download_finished, dl)) + + After the download has finished, its ``successful`` attribute can be + checked to make sure it finished successfully. If so, its contents can be + read by accessing the ``fileobj`` attribute:: + + def on_download_finished(download: downloads.TempDownload) -> None: + if download.successful: + print(download.fileobj.read()) + download.fileobj.close() """ fobj = io.BytesIO() fobj.name = 'temporary: ' + url.host() diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index 3f06121da..84e103cbd 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -36,7 +36,17 @@ def _add_module_info(func: typing.Callable) -> loader.ModuleInfo: class init: - """Decorator to mark a function to run when initializing.""" + """Decorator to mark a function to run when initializing. + + The decorated function gets called with a + :class:`qutebrowser.api.apitypes.InitContext` as argument. + + Example:: + + @hook.init() + def init(_context): + message.info("Extension initialized.") + """ def __call__(self, func: typing.Callable) -> typing.Callable: info = _add_module_info(func) @@ -48,7 +58,30 @@ class init: class config_changed: - """Decorator to get notified about changed configs.""" + """Decorator to get notified about changed configs. + + By default, the decorated function is called when any change in the config + occurs:: + + @hook.config_changed() + def on_config_changed(): + ... + + When an option name is passed, it's only called when the given option was + changed:: + + @hook.config_changed('content.javascript.enabled') + def on_config_changed(): + ... + + Alternatively, a part of an option name can be specified. In the following + snippet, ``on_config_changed`` gets called when either + ``bindings.commands`` or ``bindings.key_mappings`` have changed:: + + @hook.config_changed('bindings') + def on_config_changed(): + ... + """ def __init__(self, option_filter: str = None) -> None: self._filter = option_filter diff --git a/qutebrowser/api/interceptor.py b/qutebrowser/api/interceptor.py index a40635fca..634ae1409 100644 --- a/qutebrowser/api/interceptor.py +++ b/qutebrowser/api/interceptor.py @@ -27,7 +27,13 @@ from qutebrowser.extensions.interceptors import Request def register(interceptor: interceptors.InterceptorType) -> None: """Register a request interceptor. - Whenever a request happens, the interceptor gets called with a Request - object. + Whenever a request happens, the interceptor gets called with a + :class:`Request` object. + + Example:: + + def intercept(request: interceptor.Request) -> None: + if request.request_url.host() == 'badhost.example.com': + request.block() """ interceptors.register(interceptor) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 3bd4c55c3..029394657 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -141,14 +141,11 @@ class TabData: class AbstractAction: - """Attribute of AbstractTab for Qt WebActions. - - Class attributes (overridden by subclasses): - action_class: The class actions are defined on (QWeb{Engine,}Page) - action_base: The type of the actions (QWeb{Engine,}Page.WebAction) - """ + """Attribute ``action`` of AbstractTab for Qt WebActions.""" + # The class actions are defined on (QWeb{Engine,}Page) action_class = None # type: type + # The type of the actions (QWeb{Engine,}Page.WebAction) action_base = None # type: type def __init__(self, tab: 'AbstractTab') -> None: @@ -200,7 +197,7 @@ class AbstractAction: class AbstractPrinting: - """Attribute of AbstractTab for printing the page.""" + """Attribute ``printing`` of AbstractTab for printing the page.""" def __init__(self, tab: 'AbstractTab') -> None: self._widget = None @@ -271,7 +268,7 @@ class AbstractPrinting: class AbstractSearch(QObject): - """Attribute of AbstractTab for doing searches. + """Attribute ``search`` of AbstractTab for doing searches. Attributes: text: The last thing this view was searched for. @@ -279,15 +276,14 @@ class AbstractSearch(QObject): this view. _flags: The flags of the last search (needs to be set by subclasses). _widget: The underlying WebView widget. - - Signals: - finished: Emitted when a search was finished. - arg: True if the text was found, False otherwise. - cleared: Emitted when an existing search was cleared. """ + #: Signal emitted when a search was finished + #: (True if the text was found, False otherwise) finished = pyqtSignal(bool) + #: Signal emitted when an existing search was cleared. cleared = pyqtSignal() + _Callback = typing.Callable[[bool], None] def __init__(self, tab: 'AbstractTab', parent: QWidget = None): @@ -350,17 +346,13 @@ class AbstractSearch(QObject): class AbstractZoom(QObject): - """Attribute of AbstractTab for controlling zoom. - - Attributes: - _neighborlist: A NeighborList with the zoom levels. - _default_zoom_changed: Whether the zoom was changed from the default. - """ + """Attribute ``zoom`` of AbstractTab for controlling zoom.""" def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) self._tab = tab self._widget = None + # Whether zoom was changed from the default. self._default_zoom_changed = False self._init_neighborlist() config.instance.changed.connect(self._on_config_changed) @@ -375,7 +367,9 @@ class AbstractZoom(QObject): self._init_neighborlist() def _init_neighborlist(self) -> None: - """Initialize self._neighborlist.""" + """Initialize self._neighborlist. + + It is a NeighborList with the zoom levels.""" levels = config.val.zoom.levels self._neighborlist = usertypes.NeighborList( levels, mode=usertypes.NeighborList.Modes.edge) @@ -427,15 +421,12 @@ class AbstractZoom(QObject): class AbstractCaret(QObject): - """Attribute of AbstractTab for caret browsing. - - Signals: - selection_toggled: Emitted when the selection was toggled. - arg: Whether the selection is now active. - follow_selected_done: Emitted when a follow_selection action is done. - """ + """Attribute ``caret`` of AbstractTab for caret browsing.""" + #: Signal emitted when the selection was toggled. + #: (argument - whether the selection is now active) selection_toggled = pyqtSignal(bool) + #: Emitted when a ``follow_selection`` action is done. follow_selected_done = pyqtSignal() def __init__(self, @@ -522,16 +513,12 @@ class AbstractCaret(QObject): class AbstractScroller(QObject): - """Attribute of AbstractTab to manage scroll position. - - Signals: - perc_changed: The scroll position changed. - before_jump_requested: - Emitted by other code when the user requested a jump. - Used to set the special ' mark so the user can return. - """ + """Attribute ``scroller`` of AbstractTab to manage scroll position.""" + #: Signal emitted when the scroll position changed (int, int) perc_changed = pyqtSignal(int, int) + #: Signal emitted before the user requested a jump. + #: Used to set the special ' mark so the user can return. before_jump_requested = pyqtSignal() def __init__(self, tab: 'AbstractTab', parent: QWidget = None): @@ -833,42 +820,46 @@ class AbstractTabPrivate: class AbstractTab(QWidget): - """An adapter for QWebView/QWebEngineView representing a single tab. - - Signals: - See related Qt signals. - - new_tab_requested: Emitted when a new tab should be opened with the - given URL. - load_status_changed: The loading status changed - fullscreen_requested: Fullscreen display was requested by the page. - arg: True if fullscreen should be turned on, - False if it should be turned off. - renderer_process_terminated: Emitted when the underlying renderer - process terminated. - arg 0: A TerminationStatus member. - arg 1: The exit code. - before_load_started: Emitted before we tell Qt to open a URL. - """ + """An adapter for QWebView/QWebEngineView representing a single tab.""" + #: Signal emitted when a website requests to close this tab. window_close_requested = pyqtSignal() + #: Signal emitted when a link is hovered (the hover text) link_hovered = pyqtSignal(str) + #: Signal emitted when a page started loading load_started = pyqtSignal() + #: Signal emitted when a page is loading (progress percentage) load_progress = pyqtSignal(int) + #: Signal emitted when a page finished loading (success as bool) load_finished = pyqtSignal(bool) + #: Signal emitted when a page's favicon changed (icon as QIcon) icon_changed = pyqtSignal(QIcon) + #: Signal emitted when a page's title changed (new title as str) title_changed = pyqtSignal(str) - load_status_changed = pyqtSignal(usertypes.LoadStatus) + #: Signal emitted when a new tab should be opened (url as QUrl) new_tab_requested = pyqtSignal(QUrl) + #: Signal emitted when a page's URL changed (url as QUrl) url_changed = pyqtSignal(QUrl) - shutting_down = pyqtSignal() + #: Signal emitted when a tab's content size changed + #: (new size as QSizeF) contents_size_changed = pyqtSignal(QSizeF) - # url, requested url, title - history_item_triggered = pyqtSignal(QUrl, QUrl, str) + #: Signal emitted when a page requested full-screen (bool) fullscreen_requested = pyqtSignal(bool) - renderer_process_terminated = pyqtSignal(TerminationStatus, int) + #: Signal emitted before load starts (URL as QUrl) before_load_started = pyqtSignal(QUrl) + # Signal emitted when a page's load status changed + # (argument: usertypes.LoadStatus) + load_status_changed = pyqtSignal(usertypes.LoadStatus) + # Signal emitted before shutting down + shutting_down = pyqtSignal() + # Signal emitted when a history item should be added + history_item_triggered = pyqtSignal(QUrl, QUrl, str) + # Signal emitted when the underlying renderer process terminated. + # arg 0: A TerminationStatus member. + # arg 1: The exit code. + renderer_process_terminated = pyqtSignal(TerminationStatus, int) + def __init__(self, *, win_id: int, private: bool, parent: QWidget = None) -> None: self.is_private = private diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index a22facfbd..c00f247f6 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -141,20 +141,9 @@ class AbstractWebElement(collections.abc.MutableMapping): def rect_on_view(self, *, elem_geometry=None, no_js=False): """Get the geometry of the element relative to the webview. - Uses the getClientRects() JavaScript method to obtain the collection of - rectangles containing the element and returns the first rectangle which - is large enough (larger than 1px times 1px). If all rectangles returned - by getClientRects() are too small, falls back to elem.rect_on_view(). - - Skipping of small rectangles is due to elements containing other - elements with "display:block" style, see - https://github.com/qutebrowser/qutebrowser/issues/1298 - Args: elem_geometry: The geometry of the element, or None. - Calling QWebElement::geometry is rather expensive so - we want to avoid doing it twice. - no_js: Fall back to the Python implementation + no_js: Fall back to the Python implementation. """ raise NotImplementedError diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py index 4a3ac17d4..269c82ab8 100644 --- a/qutebrowser/extensions/interceptors.py +++ b/qutebrowser/extensions/interceptors.py @@ -34,8 +34,12 @@ class Request: """A request which can be intercepted/blocked.""" + #: The URL of the page being shown. first_party_url = attr.ib() # type: QUrl + + #: The URL of the file being requested. request_url = attr.ib() # type: QUrl + is_blocked = attr.ib(False) # type: bool def block(self) -> None: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index b496273f8..6731721aa 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -42,12 +42,12 @@ def _log_stack(typ: str, stack: str) -> None: def error(message: str, *, stack: str = None, replace: bool = False) -> None: - """Convenience function to display an error message in the statusbar. + """Display an error message. Args: - message: The message to show - stack: The stack trace to show. - replace: Replace existing messages with replace=True + message: The message to show. + stack: The stack trace to show (if any). + replace: Replace existing messages which are still being shown. """ if stack is None: stack = ''.join(traceback.format_stack()) @@ -60,11 +60,11 @@ def error(message: str, *, stack: str = None, replace: bool = False) -> None: def warning(message: str, *, replace: bool = False) -> None: - """Convenience function to display a warning message in the statusbar. + """Display a warning message. Args: - message: The message to show - replace: Replace existing messages with replace=True + message: The message to show. + replace: Replace existing messages which are still being shown. """ _log_stack('warning', ''.join(traceback.format_stack())) log.message.warning(message) @@ -72,11 +72,11 @@ def warning(message: str, *, replace: bool = False) -> None: def info(message: str, *, replace: bool = False) -> None: - """Convenience function to display an info message in the statusbar. + """Display an info message. Args: - message: The message to show - replace: Replace existing messages with replace=True + message: The message to show. + replace: Replace existing messages which are still being shown. """ log.message.info(message) global_bridge.show(usertypes.MessageLevel.info, message, replace) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index c948df48f..84b7e7f9b 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -210,15 +210,33 @@ PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert', 'download']) -# Where to open a clicked link. -ClickTarget = enum.Enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window', - 'hover']) +class ClickTarget(enum.Enum): + + """How to open a clicked link.""" + + normal = 0 #: Open the link in the current tab + tab = 1 #: Open the link in a new foreground tab + tab_bg = 2 #: Open the link in a new background tab + window = 3 #: Open the link in a new window + hover = 4 #: Only hover over the link -# Key input modes -KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', - 'insert', 'passthrough', 'caret', 'set_mark', - 'jump_mark', 'record_macro', 'run_macro']) +class KeyMode(enum.Enum): + + """Key input modes.""" + + normal = 1 #: Normal mode (no mode was entered) + hint = 2 #: Hint mode (showing labels for links) + command = 3 #: Command mode (after pressing the colon key) + yesno = 4 #: Yes/No prompts + prompt = 5 #: Text prompts + insert = 6 #: Insert mode (passing through most keys) + passthrough = 7 #: Passthrough mode (passing through all keys) + caret = 8 #: Caret mode (moving cursor with keys) + set_mark = 9 + jump_mark = 10 + record_macro = 11 + run_macro = 12 class Exit(enum.IntEnum): @@ -241,8 +259,14 @@ LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https', Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine']) -# JS world for QtWebEngine -JsWorld = enum.Enum('JsWorld', ['main', 'application', 'user', 'jseval']) +class JsWorld(enum.Enum): + + """World/context to run JavaScript code in.""" + + main = 1 #: Same world as the web page's JavaScript. + application = 2 #: Application world, used by qutebrowser internally. + user = 3 #: User world, currently not used. + jseval = 4 #: World used for the jseval-command. # Log level of a JS message. This needs to match up with the keys allowed for From a297e1e2a79c9a4df7a8407d808b11b97c48b585 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 10:39:34 +0100 Subject: [PATCH 059/117] Customize sphinx commandline options --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8fb312798..2719c137a 100644 --- a/tox.ini +++ b/tox.ini @@ -208,4 +208,4 @@ deps = -r{toxinidir}/misc/requirements/requirements-pyqt.txt -r{toxinidir}/misc/requirements/requirements-sphinx.txt commands = - {envpython} -m sphinx doc/extapi/ doc/extapi/_build/ + {envpython} -m sphinx -jauto -W --color {posargs} doc/extapi/ doc/extapi/_build/ From a9cac0895eac1b2d0be4cdd8a52506c3af036339 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 10:45:09 +0100 Subject: [PATCH 060/117] tox: Set usedevelop for sphinx --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 2719c137a..bc44df987 100644 --- a/tox.ini +++ b/tox.ini @@ -203,6 +203,7 @@ commands = [testenv:sphinx] basepython = {env:PYTHON:python3} passenv = +usedevelop = true deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyqt.txt From 5a1be2ff165bf56d50154cf08263ddda5751548e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 10:48:16 +0100 Subject: [PATCH 061/117] Order members like in source --- doc/extapi/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/extapi/conf.py b/doc/extapi/conf.py index 09105248a..4cc5c6803 100644 --- a/doc/extapi/conf.py +++ b/doc/extapi/conf.py @@ -42,6 +42,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', ] +autodoc_member_order = 'bysource' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From aec348d9293bb3fb02438a60343c6b5a0bb5398b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 11:27:38 +0100 Subject: [PATCH 062/117] Fix test_mhtml_e2e with QtWebKit + Qt 5.12 Fixup for 9eccaea09caefd5d3232ce64889c450783b9ef25 See #4025 --- tests/end2end/test_mhtml_e2e.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/end2end/test_mhtml_e2e.py b/tests/end2end/test_mhtml_e2e.py index 953824b8c..27fdd2abf 100644 --- a/tests/end2end/test_mhtml_e2e.py +++ b/tests/end2end/test_mhtml_e2e.py @@ -61,8 +61,8 @@ def normalize_line(line): return line -def normalize_whole(s): - if qtutils.version_check('5.12', compiled=False): +def normalize_whole(s, webengine): + if qtutils.version_check('5.12', compiled=False) and webengine: s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID') return s @@ -71,8 +71,9 @@ class DownloadDir: """Abstraction over a download directory.""" - def __init__(self, tmpdir): + def __init__(self, tmpdir, config): self._tmpdir = tmpdir + self._config = config self.location = str(tmpdir) def read_file(self): @@ -92,14 +93,15 @@ class DownloadDir: if normalize_line(line) is not None) actual_data = '\n'.join(normalize_line(line) for line in self.read_file()) - actual_data = normalize_whole(actual_data) + actual_data = normalize_whole(actual_data, + webengine=self._config.webengine) assert actual_data == expected_data @pytest.fixture -def download_dir(tmpdir): - return DownloadDir(tmpdir) +def download_dir(tmpdir, pytestconfig): + return DownloadDir(tmpdir, pytestconfig) def _test_mhtml_requests(test_dir, test_path, server): From e00a7ee5bec74aca7e96f0b2ec1bd3e99593934b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 11:33:55 +0100 Subject: [PATCH 063/117] Mark IPC test which is broken on macOS as xfail See #4471 --- tests/unit/misc/test_ipc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 93b6581b8..29ca0ff9d 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy import qutebrowser from qutebrowser.misc import ipc -from qutebrowser.utils import standarddir, utils +from qutebrowser.utils import standarddir, utils, qtutils from helpers import stubs @@ -630,6 +630,8 @@ class TestSendOrListen: assert ret_client is None @pytest.mark.posix(reason="Unneeded on Windows") + @pytest.mark.xfail(qtutils.version_check('5.12', compiled=False) and + utils.is_mac, reason="Broken, see #4471") def test_correct_socket_name(self, args): server = ipc.send_or_listen(args) expected_dir = ipc._get_socketname(args.basedir) From 9250f3c560ced8f05bd7260bdce832c4ff600408 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 17:19:39 +0100 Subject: [PATCH 064/117] Fix lint --- MANIFEST.in | 1 + doc/faq.asciidoc | 335 -------------------------------- qutebrowser/api/cmdutils.py | 34 ++-- qutebrowser/keyinput/modeman.py | 2 +- 4 files changed, 20 insertions(+), 352 deletions(-) delete mode 100644 doc/faq.asciidoc diff --git a/MANIFEST.in b/MANIFEST.in index 3a29ba690..cd9e50cf9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -40,5 +40,6 @@ exclude .* exclude misc/qutebrowser.spec exclude misc/qutebrowser.nsi exclude misc/qutebrowser.rcc +prune doc/extapi global-exclude __pycache__ *.pyc *.pyo diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc deleted file mode 100644 index 113a11f09..000000000 --- a/doc/faq.asciidoc +++ /dev/null @@ -1,335 +0,0 @@ -Frequently asked questions -========================== -:title: Frequently asked questions -The Compiler - -[qanda] -What is qutebrowser based on?:: - qutebrowser uses https://www.python.org/[Python], https://www.qt.io/[Qt] and - https://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. -+ -The concept of it is largely inspired by https://bitbucket.org/portix/dwb/[dwb] -and http://www.vimperator.org/vimperator[Vimperator]. Many actions and -key bindings are similar to dwb. - -Why another browser?:: - It might be hard to believe, but I didn't find any browser which I was - happy with, so I started to write my own. Also, I needed a project to get - into writing GUI applications with Python and - link:https://www.qt.io/[Qt]/link:https://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. -+ -Read the next few questions to find out why I was unhappy with existing -software. - -What's wrong with link:https://bitbucket.org/portix/dwb/[dwb]/link:https://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/jumanji/... (projects based on WebKitGTK)?:: - Most of them are based on the https://webkitgtk.org/[WebKitGTK+] - https://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API, - which causes a lot of crashes. As the GTK API using WebKit1 is - https://lists.webkit.org/pipermail/webkit-gtk/2014-March/001821.html[deprecated], - these bugs are never going to be fixed. -+ -When qutebrowser was created, the newer -https://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked -basic features like proxy support, and almost no projects have started porting -to WebKit2. In the meantime, this situation has improved a bit, but there are -still only a few projects which have some kind of WebKit2 support (see the -https://github.com/qutebrowser/qutebrowser#similar-projects[list of -alternatives]). -+ -qutebrowser uses https://www.qt.io/[Qt] and -https://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports -https://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on -Google's https://www.chromium.org/Home[Chromium]. With an up-to-date Qt, it has -much more man-power behind it than WebKitGTK+ has, and thus supports more modern -web features - it's also arguably more secure. - -What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://bug.5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?:: - Firefox likes to break compatibility with addons on each upgrade, gets - slower and more bloated with every upgrade, and has some - https://blog.mozilla.org/advancingcontent/2014/02/11/publisher-transformation-with-users-at-the-center/[horrible - ideas] lately. -+ -Also, developing addons for it is a nightmare. - -What's wrong with https://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?:: - The Chrome plugin API doesn't seem to allow much freedom for plugin - writers, which results in Vimium not really having all the features you'd - expect from a proper minimal, vim-like browser. - -Why Python?:: - I enjoy writing Python since 2011, which made it one of the possible - choices. I wanted to use https://www.qt.io/[Qt] because of - https://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have - https://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't - like C++ and can't write it very well, so that wasn't an alternative. - -But isn't Python too slow for a browser?:: - https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.] - I believe efficiency while coding is a lot more important than efficiency - while running. Also, most of the heavy lifting of qutebrowser is done by Qt - and WebKit in C++, with the - https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released. - -Is qutebrowser secure?:: - Most security issues are in the backend (which handles networking, - rendering, JavaScript, etc.) and not qutebrowser itself. -+ -qutebrowser uses https://wiki.qt.io/QtWebEngine[QtWebEngine] by default. -QtWebEngine is based on Google's https://www.chromium.org/Home[Chromium]. While -Qt only updates to a new Chromium release on every minor Qt release (all ~6 -months), every patch release backports security fixes from newer Chromium -versions. In other words: As long as you're using an up-to-date Qt, you should -be recieving security updates on a regular basis, without qutebrowser having to -do anything. Chromium's process isolation and -https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing] -features are also enabled as a second line of defense. -+ -https://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative -backend, but hasn't seen new releases -https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any -process isolation or sandboxing. See -https://github.com/qutebrowser/qutebrowser/issues/4039[#4039] for more details. -+ -Security issues in qutebrowser's code happen very rarely (as per July 2018, -there have been three security issues caused by qutebrowser in over 4.5 years). -Those were handled appropriately -(http://seclists.org/oss-sec/2018/q3/29[example]) and fixed timely. To report -security bugs, please contact me directly at mail@qutebrowser.org, GPG ID -https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072]. - -Is there an adblocker?:: - There is a host-based adblocker which takes /etc/hosts-like lists. A "real" - adblocker has a - https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big - impact] on browsing speed and - https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM - usage], so implementing support for AdBlockPlus-like lists is currently not - a priority. - -How can I get No-Script-like behavior?:: - To disable JavaScript by default: -+ ----- -:set content.javascript.enabled false ----- -+ -The basic command for enabling JavaScript for the current host is `tsh`. -This will allow JavaScript execution for the current session. -Use `S` instead of `s` to make the exception permanent. -With `H` instead of `h`, subdomains are included. -With `u` instead of `h`, only the current URL is whitelisted (not the whole host). - -How do I play Youtube videos with mpv?:: - You can easily add a key binding to play youtube videos inside a real video - player - optionally even with hinting for links: -+ ----- -:bind m spawn mpv {url} -:bind M hint links spawn mpv {hint-url} ----- -+ -Note that you might need an additional package (e.g. -https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on -Archlinux) to play web videos with mpv. -+ -There is a very useful script for mpv, which emulates "unique application" -functionality. This way you can add links to the mpv playlist instead of -playing them all at once. -+ -You can find the script here: https://github.com/mpv-player/mpv/blob/master/TOOLS/umpv -+ -It also works nicely with rapid hints: -+ ----- -:bind m spawn umpv {url} -:bind M hint links spawn umpv {hint-url} -:bind ;M hint --rapid links spawn umpv {hint-url} ----- - -How do I use qutebrowser with mutt?:: - For security reasons, local files without `.html` extensions aren't - rendered as HTML, see - https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue] - for details. You can do this in your `mailcap` file to get a proper - extension: -+ ----- - text/html; qutebrowser %s; nametemplate=%s.html ----- - -What is the difference between bookmarks and quickmarks?:: - Bookmarks will always use the title of the website as their name, but with quickmarks - you can set your own title. -+ -For example, if you bookmark multiple food recipe websites and use `:open`, -you have to type the title or address of the website. -+ -When using quickmark, you can give them all names, like -`foodrecipes1`, `foodrecipes2` and so on. When you type -`:open foodrecipes`, you will see a list of all the food recipe sites, -without having to remember the exact website title or address. - -How do I use spell checking?:: - Configuring spell checking in qutebrowser depends on the backend in use - (see https://github.com/qutebrowser/qutebrowser/issues/700[#700] for - a more detailed discussion). -+ -For QtWebKit: - -. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins]. - . Note: with QtWebKit reloaded you may experience some issues. See - https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10]. -. The dictionary to use is taken from the `DICTIONARY` environment variable. - The default is `en_US`. For example to use Dutch spell check set `DICTIONARY` - to `nl_NL`; you can't use multiple dictionaries or change them at runtime at - the moment. - (also see the README file for `qtwebkit-plugins`). -. Remember to install the hunspell dictionaries if you don't have them already - (most distros should have packages for this). - -+ -For QtWebEngine: - -. Make sure your versions of PyQt and Qt are 5.8 or higher. -. Use `dictcli.py` script to install dictionaries. - Run the script with `-h` for the parameter description. -. Set `spellcheck.languages` to the desired list of languages, e.g.: - `:set spellcheck.languages "['en-US', 'pl-PL']"` - -How do I use Tor with qutebrowser?:: - Start tor on your machine, and do `:set content.proxy socks://localhost:9050/` - in qutebrowser. Note this won't give you the same amount of fingerprinting - protection that the Tor Browser does, but it's useful to be able to access - `.onion` sites. - -Why does J move to the next (right) tab, and K to the previous (left) one?:: - One reason is because https://bitbucket.org/portix/dwb[dwb] did it that way, - and qutebrowser's keybindings are designed to be compatible with dwb's. - The rationale behind it is that J is "down" in vim, and K is "up", which - corresponds nicely to "next"/"previous". It also makes much more sense with - vertical tabs (e.g. `:set tabs.position left`). - -What's the difference between insert and passthrough mode?:: - They are quite similar, but insert mode has some bindings (like `Ctrl-e` to - open an editor) while passthrough mode only has escape bound. It might also - be useful to rebind escape to something else in passthrough mode only, to be - able to send an escape keypress to the website. - -Why does it take longer to open a URL in qutebrowser than in chromium?:: - When opening a URL in an existing instance, the normal qutebrowser - Python script is started and a few PyQt libraries need to be - loaded until it is detected that there is an instance running - to which the URL is then passed. This takes some time. - One workaround is to use this - https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script] - and place it in your $PATH with the name "qutebrowser". This - script passes the URL via an unix socket to qutebrowser (if its - running already) using socat which is much faster and starts a new - qutebrowser if it is not running already. Also check if you want - to use webengine as backend in line 17 and change it to your - needs. - -How do I make qutebrowser use greasemonkey scripts?:: - There is currently no UI elements to handle managing greasemonkey scripts. - All management of what scripts are installed or disabled is done in the - filesystem by you. qutebrowser reads all files that have an extension of - `.js` from the `/greasemonkey/` folder and attempts to load them. - Where `` is the qutebrowser data directory shown in the `Paths` - section of the page displayed by `:version`. If you want to disable a - script just rename it, for example, to have `.disabled` on the end, after - the `.js` extension. To reload scripts from that directory run the command - `:greasemonkey-reload`. -+ -Troubleshooting: to check that your script is being loaded when -`:greasemonkey-reload` runs you can start qutebrowser with the arguments -`--debug --logfilter greasemonkey,js` and check the messages on the -program's standard output for errors parsing or loading your script. -You may also see javascript errors if your script is expecting an environment -that we fail to provide. -+ -Note that there are some missing features which you may run into: - -. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource - Sharing restrictions, this is currently not supported, so scripts making - requests to third party sites will often fail to function correctly. -. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular - expressions are not supported in `@include` or `@exclude` rules. If your - script uses them you can re-write them to use glob expressions or convert - them to `@match` rules. - See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info. -. Any greasemonkey API function to do with adding UI elements is not currently - supported. That means context menu extentensions and background pages. - -How do I change the `WM_CLASS` used by qutebrowser windows?:: - Qt only supports setting `WM_CLASS` globally, which you can do by starting - with `--qt-arg name foo`. Note that all windows are part of the same - qutebrowser instance (unless you use `--temp-basedir` or `--basedir`), so - they all will share the same `WM_CLASS`. - -== Troubleshooting - -Unable to view flash content.:: - If you have flash installed for on your system, it's necessary to enable plugins - to use the flash plugin. Using the command `:set content.plugins true` - in qutebrowser will enable plugins. Packages for flash should - be provided for your platform or it can be obtained from - https://get.adobe.com/flashplayer/[Adobe]. - -Experiencing freezing on sites like duckduckgo and youtube.:: - This issue could be caused by stale plugin files installed by `mozplugger` - if mozplugger was subsequently removed. - Try exiting qutebrowser and removing `~/.mozilla/plugins/mozplugger*.so`. - See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] - for more details. - -When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro:: - As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + - As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + - On gentoo, you just need to add it into your make.conf, like this: + - - CFLAGS="... -fno-delete-null-pointer-checks" - CXXFLAGS="... -fno-delete-null-pointer-checks" -+ -And then re-emerging qtwebengine with: + - - emerge -1 qtwebengine - -Unable to view DRM content (Netflix, Spotify, etc.).:: - You will need to install `widevine` and set `qt.args` to point to it. - Qt 5.9 currently only supports widevine up to Chrome version 61. -+ -On Arch, simply install `qt5-webengine-widevine` from the AUR and run: -+ ----- -:set qt.args '["ppapi-widevine-path=/usr/lib/qt/plugins/ppapi/libwidevinecdmadapter.so"]' -:restart ----- -+ -For other distributions, download the chromium tarball and widevine-cdm zip from -https://aur.archlinux.org/packages/qt5-webengine-widevine/[the AUR page], -extract `libwidevinecdmadapter.so` and `libwidevinecdm.so` files, respectively, -and move them to the `ppapi` plugin directory in your Qt library directory (create it if it does not exist). -+ -Lastly, set your `qt.args` to point to that directory and restart qutebrowser: -+ ----- -:set qt.args '["ppapi-widevine-path=/usr/lib64/qt5/plugins/ppapi/libwidevinecdmadapter.so"]' -:restart ----- - -Unable to use `spawn` on MacOS.:: -When running qutebrowser from the prebuilt binary (`qutebrowser.app`) it *will -not* read any files that would alter your `$PATH` (e.g. `.profile`, `.bashrc`, -etc). This is not a bug, just that `.profile` is not propogated to GUI -applications in MacOS. -+ -See https://github.com/qutebrowser/qutebrowser/issues/4273[Issue #4273] for -details and potential workarounds. - -My issue is not listed.:: - If you experience any segfaults or crashes, you can report the issue in - https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or - using the `:report` command. - If you are reporting a segfault, make sure you read the - link:stacktrace{outfilesuffix}[guide] on how to report them with all needed - information. diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index f6a6f6da9..cd43079ad 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -17,9 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -""" -qutebrowser has the concept of functions which are exposed to the user as -commands. +"""qutebrowser has the concept of functions, exposed to the user as commands. Creating a new command is straightforward:: @@ -43,7 +41,8 @@ The type can be overridden using Python's function annotations:: Possible values: -- A callable (``int``, ``float``, etc.): Gets called to validate/convert the value. +- A callable (``int``, ``float``, etc.): Gets called to validate/convert the + value. - A python enum type: All members of the enum are possible values. - A ``typing.Union`` of multiple types above: Any of these types are valid values, e.g., ``typing.Union[str, int]``. @@ -159,40 +158,43 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to customize an argument. - You can customize how an argument is handled using the ``@cmdutils.argument`` - decorator *after* ``@cmdutils.register``. This can, for example, be used to - customize the flag an argument should get:: + You can customize how an argument is handled using the + ``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can, + for example, be used to customize the flag an argument should get:: @cmdutils.register(...) @cmdutils.argument('bar', flag='c') def foo(bar): ... - For a ``str`` argument, you can restrict the allowed strings using ``choices``:: + For a ``str`` argument, you can restrict the allowed strings using + ``choices``:: @cmdutils.register(...) @cmdutils.argument('bar', choices=['val1', 'val2']) def foo(bar: str): ... - For ``typing.Union`` types, the given ``choices`` are only checked if other types - (like ``int``) don't match. + For ``typing.Union`` types, the given ``choices`` are only checked if other + types (like ``int``) don't match. The following arguments are supported for ``@cmdutils.argument``: - ``flag``: Customize the short flag (``-x``) the argument will get. - ``value``: Tell qutebrowser to fill the argument with special values: - * ``value=cmdutils.Value.count``: The ``count`` given by the user to the command. + * ``value=cmdutils.Value.count``: The ``count`` given by the user to the + command. * ``value=cmdutils.Value.win_id``: The window ID of the current window. - * ``value=cmdutils.Value.cur_tab``: The tab object which is currently focused. + * ``value=cmdutils.Value.cur_tab``: The tab object which is currently + focused. - - ``completion``: A completion function to use when completing arguments for - the given command. + - ``completion``: A completion function to use when completing arguments + for the given command. - ``choices``: The allowed string choices for the argument. - The name of an argument will always be the parameter name, with any trailing - underscores stripped and underscores replaced by dashes. + The name of an argument will always be the parameter name, with any + trailing underscores stripped and underscores replaced by dashes. """ def __init__(self, argname: str, **kwargs: typing.Any) -> None: diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index c06700b6c..edb443eec 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -64,7 +64,7 @@ class NotInModeError(Exception): def init(win_id, parent): """Initialize the mode manager and the keyparsers for the given win_id.""" - KM = usertypes.KeyMode # noqa: N801,N806 pylint: disable=invalid-name + KM = usertypes.KeyMode # noqa: N806 modeman = ModeManager(win_id, parent) objreg.register('mode-manager', modeman, scope='window', window=win_id) keyparsers = { From 45f78ce2c76b8dbd9bcf399e5daf8aff57c9c223 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Dec 2018 18:23:43 +0100 Subject: [PATCH 065/117] Restore FAQ --- doc/faq.asciidoc | 335 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 doc/faq.asciidoc diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc new file mode 100644 index 000000000..113a11f09 --- /dev/null +++ b/doc/faq.asciidoc @@ -0,0 +1,335 @@ +Frequently asked questions +========================== +:title: Frequently asked questions +The Compiler + +[qanda] +What is qutebrowser based on?:: + qutebrowser uses https://www.python.org/[Python], https://www.qt.io/[Qt] and + https://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. ++ +The concept of it is largely inspired by https://bitbucket.org/portix/dwb/[dwb] +and http://www.vimperator.org/vimperator[Vimperator]. Many actions and +key bindings are similar to dwb. + +Why another browser?:: + It might be hard to believe, but I didn't find any browser which I was + happy with, so I started to write my own. Also, I needed a project to get + into writing GUI applications with Python and + link:https://www.qt.io/[Qt]/link:https://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. ++ +Read the next few questions to find out why I was unhappy with existing +software. + +What's wrong with link:https://bitbucket.org/portix/dwb/[dwb]/link:https://sourceforge.net/projects/vimprobable/[vimprobable]/link:https://mason-larobina.github.io/luakit/[luakit]/jumanji/... (projects based on WebKitGTK)?:: + Most of them are based on the https://webkitgtk.org/[WebKitGTK+] + https://webkitgtk.org/reference/webkitgtk/stable/index.html[WebKit1] API, + which causes a lot of crashes. As the GTK API using WebKit1 is + https://lists.webkit.org/pipermail/webkit-gtk/2014-March/001821.html[deprecated], + these bugs are never going to be fixed. ++ +When qutebrowser was created, the newer +https://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked +basic features like proxy support, and almost no projects have started porting +to WebKit2. In the meantime, this situation has improved a bit, but there are +still only a few projects which have some kind of WebKit2 support (see the +https://github.com/qutebrowser/qutebrowser#similar-projects[list of +alternatives]). ++ +qutebrowser uses https://www.qt.io/[Qt] and +https://wiki.qt.io/QtWebEngine[QtWebEngine] by default (and supports +https://wiki.qt.io/QtWebKit[QtWebKit] optionally). QtWebEngine is based on +Google's https://www.chromium.org/Home[Chromium]. With an up-to-date Qt, it has +much more man-power behind it than WebKitGTK+ has, and thus supports more modern +web features - it's also arguably more secure. + +What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://bug.5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?:: + Firefox likes to break compatibility with addons on each upgrade, gets + slower and more bloated with every upgrade, and has some + https://blog.mozilla.org/advancingcontent/2014/02/11/publisher-transformation-with-users-at-the-center/[horrible + ideas] lately. ++ +Also, developing addons for it is a nightmare. + +What's wrong with https://www.chromium.org/Home[Chromium] and https://vimium.github.io/[Vimium]?:: + The Chrome plugin API doesn't seem to allow much freedom for plugin + writers, which results in Vimium not really having all the features you'd + expect from a proper minimal, vim-like browser. + +Why Python?:: + I enjoy writing Python since 2011, which made it one of the possible + choices. I wanted to use https://www.qt.io/[Qt] because of + https://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have + https://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't + like C++ and can't write it very well, so that wasn't an alternative. + +But isn't Python too slow for a browser?:: + https://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.] + I believe efficiency while coding is a lot more important than efficiency + while running. Also, most of the heavy lifting of qutebrowser is done by Qt + and WebKit in C++, with the + https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released. + +Is qutebrowser secure?:: + Most security issues are in the backend (which handles networking, + rendering, JavaScript, etc.) and not qutebrowser itself. ++ +qutebrowser uses https://wiki.qt.io/QtWebEngine[QtWebEngine] by default. +QtWebEngine is based on Google's https://www.chromium.org/Home[Chromium]. While +Qt only updates to a new Chromium release on every minor Qt release (all ~6 +months), every patch release backports security fixes from newer Chromium +versions. In other words: As long as you're using an up-to-date Qt, you should +be recieving security updates on a regular basis, without qutebrowser having to +do anything. Chromium's process isolation and +https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing] +features are also enabled as a second line of defense. ++ +https://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative +backend, but hasn't seen new releases +https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any +process isolation or sandboxing. See +https://github.com/qutebrowser/qutebrowser/issues/4039[#4039] for more details. ++ +Security issues in qutebrowser's code happen very rarely (as per July 2018, +there have been three security issues caused by qutebrowser in over 4.5 years). +Those were handled appropriately +(http://seclists.org/oss-sec/2018/q3/29[example]) and fixed timely. To report +security bugs, please contact me directly at mail@qutebrowser.org, GPG ID +https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072]. + +Is there an adblocker?:: + There is a host-based adblocker which takes /etc/hosts-like lists. A "real" + adblocker has a + https://www.reddit.com/r/programming/comments/25j41u/adblock_pluss_effect_on_firefoxs_memory_usage/chhpomw[big + impact] on browsing speed and + https://blog.mozilla.org/nnethercote/2014/05/14/adblock-pluss-effect-on-firefoxs-memory-usage/[RAM + usage], so implementing support for AdBlockPlus-like lists is currently not + a priority. + +How can I get No-Script-like behavior?:: + To disable JavaScript by default: ++ +---- +:set content.javascript.enabled false +---- ++ +The basic command for enabling JavaScript for the current host is `tsh`. +This will allow JavaScript execution for the current session. +Use `S` instead of `s` to make the exception permanent. +With `H` instead of `h`, subdomains are included. +With `u` instead of `h`, only the current URL is whitelisted (not the whole host). + +How do I play Youtube videos with mpv?:: + You can easily add a key binding to play youtube videos inside a real video + player - optionally even with hinting for links: ++ +---- +:bind m spawn mpv {url} +:bind M hint links spawn mpv {hint-url} +---- ++ +Note that you might need an additional package (e.g. +https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on +Archlinux) to play web videos with mpv. ++ +There is a very useful script for mpv, which emulates "unique application" +functionality. This way you can add links to the mpv playlist instead of +playing them all at once. ++ +You can find the script here: https://github.com/mpv-player/mpv/blob/master/TOOLS/umpv ++ +It also works nicely with rapid hints: ++ +---- +:bind m spawn umpv {url} +:bind M hint links spawn umpv {hint-url} +:bind ;M hint --rapid links spawn umpv {hint-url} +---- + +How do I use qutebrowser with mutt?:: + For security reasons, local files without `.html` extensions aren't + rendered as HTML, see + https://bugs.chromium.org/p/chromium/issues/detail?id=777737[this Chromium issue] + for details. You can do this in your `mailcap` file to get a proper + extension: ++ +---- + text/html; qutebrowser %s; nametemplate=%s.html +---- + +What is the difference between bookmarks and quickmarks?:: + Bookmarks will always use the title of the website as their name, but with quickmarks + you can set your own title. ++ +For example, if you bookmark multiple food recipe websites and use `:open`, +you have to type the title or address of the website. ++ +When using quickmark, you can give them all names, like +`foodrecipes1`, `foodrecipes2` and so on. When you type +`:open foodrecipes`, you will see a list of all the food recipe sites, +without having to remember the exact website title or address. + +How do I use spell checking?:: + Configuring spell checking in qutebrowser depends on the backend in use + (see https://github.com/qutebrowser/qutebrowser/issues/700[#700] for + a more detailed discussion). ++ +For QtWebKit: + +. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins]. + . Note: with QtWebKit reloaded you may experience some issues. See + https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10]. +. The dictionary to use is taken from the `DICTIONARY` environment variable. + The default is `en_US`. For example to use Dutch spell check set `DICTIONARY` + to `nl_NL`; you can't use multiple dictionaries or change them at runtime at + the moment. + (also see the README file for `qtwebkit-plugins`). +. Remember to install the hunspell dictionaries if you don't have them already + (most distros should have packages for this). + ++ +For QtWebEngine: + +. Make sure your versions of PyQt and Qt are 5.8 or higher. +. Use `dictcli.py` script to install dictionaries. + Run the script with `-h` for the parameter description. +. Set `spellcheck.languages` to the desired list of languages, e.g.: + `:set spellcheck.languages "['en-US', 'pl-PL']"` + +How do I use Tor with qutebrowser?:: + Start tor on your machine, and do `:set content.proxy socks://localhost:9050/` + in qutebrowser. Note this won't give you the same amount of fingerprinting + protection that the Tor Browser does, but it's useful to be able to access + `.onion` sites. + +Why does J move to the next (right) tab, and K to the previous (left) one?:: + One reason is because https://bitbucket.org/portix/dwb[dwb] did it that way, + and qutebrowser's keybindings are designed to be compatible with dwb's. + The rationale behind it is that J is "down" in vim, and K is "up", which + corresponds nicely to "next"/"previous". It also makes much more sense with + vertical tabs (e.g. `:set tabs.position left`). + +What's the difference between insert and passthrough mode?:: + They are quite similar, but insert mode has some bindings (like `Ctrl-e` to + open an editor) while passthrough mode only has escape bound. It might also + be useful to rebind escape to something else in passthrough mode only, to be + able to send an escape keypress to the website. + +Why does it take longer to open a URL in qutebrowser than in chromium?:: + When opening a URL in an existing instance, the normal qutebrowser + Python script is started and a few PyQt libraries need to be + loaded until it is detected that there is an instance running + to which the URL is then passed. This takes some time. + One workaround is to use this + https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script] + and place it in your $PATH with the name "qutebrowser". This + script passes the URL via an unix socket to qutebrowser (if its + running already) using socat which is much faster and starts a new + qutebrowser if it is not running already. Also check if you want + to use webengine as backend in line 17 and change it to your + needs. + +How do I make qutebrowser use greasemonkey scripts?:: + There is currently no UI elements to handle managing greasemonkey scripts. + All management of what scripts are installed or disabled is done in the + filesystem by you. qutebrowser reads all files that have an extension of + `.js` from the `/greasemonkey/` folder and attempts to load them. + Where `` is the qutebrowser data directory shown in the `Paths` + section of the page displayed by `:version`. If you want to disable a + script just rename it, for example, to have `.disabled` on the end, after + the `.js` extension. To reload scripts from that directory run the command + `:greasemonkey-reload`. ++ +Troubleshooting: to check that your script is being loaded when +`:greasemonkey-reload` runs you can start qutebrowser with the arguments +`--debug --logfilter greasemonkey,js` and check the messages on the +program's standard output for errors parsing or loading your script. +You may also see javascript errors if your script is expecting an environment +that we fail to provide. ++ +Note that there are some missing features which you may run into: + +. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource + Sharing restrictions, this is currently not supported, so scripts making + requests to third party sites will often fail to function correctly. +. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular + expressions are not supported in `@include` or `@exclude` rules. If your + script uses them you can re-write them to use glob expressions or convert + them to `@match` rules. + See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info. +. Any greasemonkey API function to do with adding UI elements is not currently + supported. That means context menu extentensions and background pages. + +How do I change the `WM_CLASS` used by qutebrowser windows?:: + Qt only supports setting `WM_CLASS` globally, which you can do by starting + with `--qt-arg name foo`. Note that all windows are part of the same + qutebrowser instance (unless you use `--temp-basedir` or `--basedir`), so + they all will share the same `WM_CLASS`. + +== Troubleshooting + +Unable to view flash content.:: + If you have flash installed for on your system, it's necessary to enable plugins + to use the flash plugin. Using the command `:set content.plugins true` + in qutebrowser will enable plugins. Packages for flash should + be provided for your platform or it can be obtained from + https://get.adobe.com/flashplayer/[Adobe]. + +Experiencing freezing on sites like duckduckgo and youtube.:: + This issue could be caused by stale plugin files installed by `mozplugger` + if mozplugger was subsequently removed. + Try exiting qutebrowser and removing `~/.mozilla/plugins/mozplugger*.so`. + See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] + for more details. + +When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro:: + As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + + As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + + On gentoo, you just need to add it into your make.conf, like this: + + + CFLAGS="... -fno-delete-null-pointer-checks" + CXXFLAGS="... -fno-delete-null-pointer-checks" ++ +And then re-emerging qtwebengine with: + + + emerge -1 qtwebengine + +Unable to view DRM content (Netflix, Spotify, etc.).:: + You will need to install `widevine` and set `qt.args` to point to it. + Qt 5.9 currently only supports widevine up to Chrome version 61. ++ +On Arch, simply install `qt5-webengine-widevine` from the AUR and run: ++ +---- +:set qt.args '["ppapi-widevine-path=/usr/lib/qt/plugins/ppapi/libwidevinecdmadapter.so"]' +:restart +---- ++ +For other distributions, download the chromium tarball and widevine-cdm zip from +https://aur.archlinux.org/packages/qt5-webengine-widevine/[the AUR page], +extract `libwidevinecdmadapter.so` and `libwidevinecdm.so` files, respectively, +and move them to the `ppapi` plugin directory in your Qt library directory (create it if it does not exist). ++ +Lastly, set your `qt.args` to point to that directory and restart qutebrowser: ++ +---- +:set qt.args '["ppapi-widevine-path=/usr/lib64/qt5/plugins/ppapi/libwidevinecdmadapter.so"]' +:restart +---- + +Unable to use `spawn` on MacOS.:: +When running qutebrowser from the prebuilt binary (`qutebrowser.app`) it *will +not* read any files that would alter your `$PATH` (e.g. `.profile`, `.bashrc`, +etc). This is not a bug, just that `.profile` is not propogated to GUI +applications in MacOS. ++ +See https://github.com/qutebrowser/qutebrowser/issues/4273[Issue #4273] for +details and potential workarounds. + +My issue is not listed.:: + If you experience any segfaults or crashes, you can report the issue in + https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or + using the `:report` command. + If you are reporting a segfault, make sure you read the + link:stacktrace{outfilesuffix}[guide] on how to report them with all needed + information. From 7c486a76f8b3806bc32942e323583e0f39274505 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Dec 2018 10:45:01 +0100 Subject: [PATCH 066/117] tox: Use absolute sphinx paths --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index bc44df987..75a00961e 100644 --- a/tox.ini +++ b/tox.ini @@ -209,4 +209,4 @@ deps = -r{toxinidir}/misc/requirements/requirements-pyqt.txt -r{toxinidir}/misc/requirements/requirements-sphinx.txt commands = - {envpython} -m sphinx -jauto -W --color {posargs} doc/extapi/ doc/extapi/_build/ + {envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/ From 81375b30292d3df0b6b2f8f90c49bd1471a65290 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Dec 2018 11:25:46 +0100 Subject: [PATCH 067/117] Add type annotations for webelem/webkitelem/webengineelem --- mypy.ini | 12 ++ qutebrowser/browser/webelem.py | 107 ++++++++++-------- .../browser/webengine/webengineelem.py | 71 +++++++----- qutebrowser/browser/webkit/webkitelem.py | 88 +++++++------- 4 files changed, 165 insertions(+), 113 deletions(-) diff --git a/mypy.ini b/mypy.ini index b8b2cf16f..d8c7221ad 100644 --- a/mypy.ini +++ b/mypy.ini @@ -73,3 +73,15 @@ disallow_incomplete_defs = True [mypy-qutebrowser.extensions.*] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.browser.webelem] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.browser.webkit.webkitelem] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.browser.webengine.webengineelem] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index c00f247f6..9ae05639e 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -19,15 +19,23 @@ """Generic web element related code.""" +import typing import collections.abc -from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer +from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint from PyQt5.QtGui import QMouseEvent from qutebrowser.config import config from qutebrowser.keyinput import modeman from qutebrowser.mainwindow import mainwindow from qutebrowser.utils import log, usertypes, utils, qtutils, objreg +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + from qutebrowser.browser import browsertab + + +JsValueType = typing.Union[int, float, str, None] class Error(Exception): @@ -40,7 +48,7 @@ class OrphanedError(Error): """Raised when a webelement's parent has vanished.""" -def css_selector(group, url): +def css_selector(group: str, url: QUrl) -> str: """Get a CSS selector for the given group/URL.""" selectors = config.instance.get('hints.selectors', url) if group not in selectors: @@ -60,70 +68,72 @@ class AbstractWebElement(collections.abc.MutableMapping): tab: The tab associated with this element. """ - def __init__(self, tab): + def __init__(self, tab: 'browsertab.AbstractTab') -> None: self._tab = tab - def __eq__(self, other): + def __eq__(self, other: object) -> bool: raise NotImplementedError - def __str__(self): + def __str__(self) -> str: raise NotImplementedError - def __getitem__(self, key): + def __getitem__(self, key: str) -> str: raise NotImplementedError - def __setitem__(self, key, val): + def __setitem__(self, key: str, val: str) -> None: raise NotImplementedError - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: raise NotImplementedError - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: raise NotImplementedError - def __len__(self): + def __len__(self) -> int: raise NotImplementedError - def __repr__(self): + def __repr__(self) -> str: try: html = utils.compact_text(self.outer_xml(), 500) except Error: html = None return utils.get_repr(self, html=html) - def has_frame(self): + def has_frame(self) -> bool: """Check if this element has a valid frame attached.""" raise NotImplementedError - def geometry(self): + def geometry(self) -> QRect: """Get the geometry for this element.""" raise NotImplementedError - def classes(self): + def classes(self) -> typing.List[str]: """Get a list of classes assigned to this element.""" raise NotImplementedError - def tag_name(self): + def tag_name(self) -> str: """Get the tag name of this element. The returned name will always be lower-case. """ raise NotImplementedError - def outer_xml(self): + def outer_xml(self) -> str: """Get the full HTML representation of this element.""" raise NotImplementedError - def value(self): + def value(self) -> JsValueType: """Get the value attribute for this element, or None.""" raise NotImplementedError - def set_value(self, value): + def set_value(self, value: JsValueType) -> None: """Set the element value.""" raise NotImplementedError - def dispatch_event(self, event, bubbles=False, - cancelable=False, composed=False): + def dispatch_event(self, event: str, + bubbles: bool = False, + cancelable: bool = False, + composed: bool = False) -> None: """Dispatch an event to the element. Args: @@ -134,11 +144,12 @@ class AbstractWebElement(collections.abc.MutableMapping): """ raise NotImplementedError - def insert_text(self, text): + def insert_text(self, text: str) -> None: """Insert the given text into the element.""" raise NotImplementedError - def rect_on_view(self, *, elem_geometry=None, no_js=False): + def rect_on_view(self, *, elem_geometry: QRect = None, + no_js: bool = False) -> QRect: """Get the geometry of the element relative to the webview. Args: @@ -147,11 +158,11 @@ class AbstractWebElement(collections.abc.MutableMapping): """ raise NotImplementedError - def is_writable(self): + def is_writable(self) -> bool: """Check whether an element is writable.""" return not ('disabled' in self or 'readonly' in self) - def is_content_editable(self): + def is_content_editable(self) -> bool: """Check if an element has a contenteditable attribute. Args: @@ -166,7 +177,7 @@ class AbstractWebElement(collections.abc.MutableMapping): except KeyError: return False - def _is_editable_object(self): + def _is_editable_object(self) -> bool: """Check if an object-element is editable.""" if 'type' not in self: log.webelem.debug(" without type clicked...") @@ -182,7 +193,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # Image/Audio/... return False - def _is_editable_input(self): + def _is_editable_input(self) -> bool: """Check if an input-element is editable. Return: @@ -199,7 +210,7 @@ class AbstractWebElement(collections.abc.MutableMapping): else: return False - def _is_editable_classes(self): + def _is_editable_classes(self) -> bool: """Check if an element is editable based on its classes. Return: @@ -218,7 +229,7 @@ class AbstractWebElement(collections.abc.MutableMapping): return True return False - def is_editable(self, strict=False): + def is_editable(self, strict: bool = False) -> bool: """Check whether we should switch to insert mode for this element. Args: @@ -249,17 +260,17 @@ class AbstractWebElement(collections.abc.MutableMapping): return self._is_editable_classes() and not strict return False - def is_text_input(self): + def is_text_input(self) -> bool: """Check if this element is some kind of text box.""" roles = ('combobox', 'textbox') tag = self.tag_name() return self.get('role', None) in roles or tag in ['input', 'textarea'] - def remove_blank_target(self): + def remove_blank_target(self) -> None: """Remove target from link.""" raise NotImplementedError - def resolve_url(self, baseurl): + def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]: """Resolve the URL in the element's src/href attribute. Args: @@ -286,16 +297,16 @@ class AbstractWebElement(collections.abc.MutableMapping): qtutils.ensure_valid(url) return url - def is_link(self): + def is_link(self) -> bool: """Return True if this AbstractWebElement is a link.""" href_tags = ['a', 'area', 'link'] return self.tag_name() in href_tags and 'href' in self - def _requires_user_interaction(self): + def _requires_user_interaction(self) -> bool: """Return True if clicking this element needs user interaction.""" raise NotImplementedError - def _mouse_pos(self): + def _mouse_pos(self) -> QPoint: """Get the position to click/hover.""" # Click the center of the largest square fitting into the top/left # corner of the rectangle, this will help if part of the element @@ -311,35 +322,38 @@ class AbstractWebElement(collections.abc.MutableMapping): raise Error("Element position is out of view!") return pos - def _move_text_cursor(self): + def _move_text_cursor(self) -> None: """Move cursor to end after clicking.""" raise NotImplementedError - def _click_fake_event(self, click_target): + def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None: """Send a fake click event to the element.""" pos = self._mouse_pos() log.webelem.debug("Sending fake click to {!r} at position {} with " "target {}".format(self, pos, click_target)) - modifiers = { + target_modifiers = { usertypes.ClickTarget.normal: Qt.NoModifier, usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier, usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier, } if config.val.tabs.background: - modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier + target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier else: - modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier + target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier + + modifiers = typing.cast(Qt.KeyboardModifiers, + target_modifiers[click_target]) events = [ QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier), QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton, - Qt.LeftButton, modifiers[click_target]), + Qt.LeftButton, modifiers), QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton, - Qt.NoButton, modifiers[click_target]), + Qt.NoButton, modifiers), ] for evt in events: @@ -347,15 +361,15 @@ class AbstractWebElement(collections.abc.MutableMapping): QTimer.singleShot(0, self._move_text_cursor) - def _click_editable(self, click_target): + def _click_editable(self, click_target: usertypes.ClickTarget) -> None: """Fake a click on an editable input field.""" raise NotImplementedError - def _click_js(self, click_target): + def _click_js(self, click_target: usertypes.ClickTarget) -> None: """Fake a click by using the JS .click() method.""" raise NotImplementedError - def _click_href(self, click_target): + def _click_href(self, click_target: usertypes.ClickTarget) -> None: """Fake a click on an element with a href by opening the link.""" baseurl = self._tab.url() url = self.resolve_url(baseurl) @@ -377,7 +391,8 @@ class AbstractWebElement(collections.abc.MutableMapping): else: raise ValueError("Unknown ClickTarget {}".format(click_target)) - def click(self, click_target, *, force_event=False): + def click(self, click_target: usertypes.ClickTarget, *, + force_event: bool = False) -> None: """Simulate a click on the element. Args: @@ -414,7 +429,7 @@ class AbstractWebElement(collections.abc.MutableMapping): else: raise ValueError("Unknown ClickTarget {}".format(click_target)) - def hover(self): + def hover(self) -> None: """Simulate a mouse hover over the element.""" pos = self._mouse_pos() event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 4ef20da18..13292b45b 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -22,20 +22,27 @@ """QtWebEngine specific part of the web element API.""" +import typing + from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEngineSettings -from qutebrowser.utils import log, javascript, urlutils +from qutebrowser.utils import log, javascript, urlutils, usertypes from qutebrowser.browser import webelem +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + from qutebrowser.browser.webengine import webenginetab class WebEngineElement(webelem.AbstractWebElement): """A web element for QtWebEngine, using JS under the hood.""" - def __init__(self, js_dict, tab): + def __init__(self, js_dict: typing.Dict[str, typing.Any], + tab: 'webenginetab.WebEngineTab') -> None: super().__init__(tab) # Do some sanity checks on the data we get from JS js_dict_types = { @@ -48,7 +55,7 @@ class WebEngineElement(webelem.AbstractWebElement): 'rects': list, 'attributes': dict, 'caret_position': (int, type(None)), - } + } # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]] assert set(js_dict.keys()).issubset(js_dict_types.keys()) for name, typ in js_dict_types.items(): if name in js_dict and not isinstance(js_dict[name], typ): @@ -73,50 +80,51 @@ class WebEngineElement(webelem.AbstractWebElement): self._id = js_dict['id'] self._js_dict = js_dict - def __str__(self): + def __str__(self) -> str: return self._js_dict.get('text', '') - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if not isinstance(other, WebEngineElement): return NotImplemented return self._id == other._id # pylint: disable=protected-access - def __getitem__(self, key): + def __getitem__(self, key: str) -> str: attrs = self._js_dict['attributes'] return attrs[key] - def __setitem__(self, key, val): + def __setitem__(self, key: str, val: str) -> None: self._js_dict['attributes'][key] = val self._js_call('set_attribute', key, val) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: log.stub() - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: return iter(self._js_dict['attributes']) - def __len__(self): + def __len__(self) -> int: return len(self._js_dict['attributes']) - def _js_call(self, name, *args, callback=None): + def _js_call(self, name: str, *args: webelem.JsValueType, + callback: typing.Callable[[typing.Any], None] = None) -> None: """Wrapper to run stuff from webelem.js.""" if self._tab.is_deleted(): raise webelem.OrphanedError("Tab containing element vanished") js_code = javascript.assemble('webelem', name, self._id, *args) self._tab.run_js_async(js_code, callback=callback) - def has_frame(self): + def has_frame(self) -> bool: return True - def geometry(self): + def geometry(self) -> QRect: log.stub() return QRect() - def classes(self): + def classes(self) -> typing.List[str]: """Get a list of classes assigned to this element.""" return self._js_dict['class_name'].split() - def tag_name(self): + def tag_name(self) -> str: """Get the tag name of this element. The returned name will always be lower-case. @@ -125,34 +133,37 @@ class WebEngineElement(webelem.AbstractWebElement): assert isinstance(tag, str), tag return tag.lower() - def outer_xml(self): + def outer_xml(self) -> str: """Get the full HTML representation of this element.""" return self._js_dict['outer_xml'] - def value(self): + def value(self) -> webelem.JsValueType: return self._js_dict.get('value', None) - def set_value(self, value): + def set_value(self, value: webelem.JsValueType) -> None: self._js_call('set_value', value) - def dispatch_event(self, event, bubbles=False, - cancelable=False, composed=False): + def dispatch_event(self, event: str, + bubbles: bool = False, + cancelable: bool = False, + composed: bool = False) -> None: self._js_call('dispatch_event', event, bubbles, cancelable, composed) - def caret_position(self): + def caret_position(self) -> typing.Optional[int]: """Get the text caret position for the current element. If the element is not a text element, None is returned. """ return self._js_dict.get('caret_position', None) - def insert_text(self, text): + def insert_text(self, text: str) -> None: if not self.is_editable(strict=True): raise webelem.Error("Element is not editable!") log.webelem.debug("Inserting text into element {!r}".format(self)) self._js_call('insert_text', text) - def rect_on_view(self, *, elem_geometry=None, no_js=False): + def rect_on_view(self, *, elem_geometry: QRect = None, + no_js: bool = False) -> QRect: """Get the geometry of the element relative to the webview. Skipping of small rectangles is due to elements containing other @@ -193,16 +204,16 @@ class WebEngineElement(webelem.AbstractWebElement): self, rects)) return QRect() - def remove_blank_target(self): + def remove_blank_target(self) -> None: if self._js_dict['attributes'].get('target') == '_blank': self._js_dict['attributes']['target'] = '_top' self._js_call('remove_blank_target') - def _move_text_cursor(self): + def _move_text_cursor(self) -> None: if self.is_text_input() and self.is_editable(): self._js_call('move_cursor_to_end') - def _requires_user_interaction(self): + def _requires_user_interaction(self) -> bool: baseurl = self._tab.url() url = self.resolve_url(baseurl) if url is None: @@ -211,7 +222,7 @@ class WebEngineElement(webelem.AbstractWebElement): return False return url.scheme() not in urlutils.WEBENGINE_SCHEMES - def _click_editable(self, click_target): + def _click_editable(self, click_target: usertypes.ClickTarget) -> None: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515 ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0), QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton, @@ -221,10 +232,11 @@ class WebEngineElement(webelem.AbstractWebElement): self._js_call('focus') self._move_text_cursor() - def _click_js(self, _click_target): + def _click_js(self, _click_target: usertypes.ClickTarget) -> None: # FIXME:qtwebengine Have a proper API for this # pylint: disable=protected-access view = self._tab._widget + assert view is not None # pylint: enable=protected-access attribute = QWebEngineSettings.JavascriptCanOpenWindows could_open_windows = view.settings().testAttribute(attribute) @@ -238,8 +250,9 @@ class WebEngineElement(webelem.AbstractWebElement): qapp.processEvents(QEventLoop.ExcludeSocketNotifiers | QEventLoop.ExcludeUserInputEvents) - def reset_setting(_arg): + def reset_setting(_arg: typing.Any) -> None: """Set the JavascriptCanOpenWindows setting to its old value.""" + assert view is not None try: view.settings().setAttribute(attribute, could_open_windows) except RuntimeError: diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 66d5e59b8..44cc8ae2d 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -19,12 +19,18 @@ """QtWebKit specific part of the web element API.""" +import typing + from PyQt5.QtCore import QRect from PyQt5.QtWebKit import QWebElement, QWebSettings +from PyQt5.QtWebKitWidgets import QWebFrame from qutebrowser.config import config -from qutebrowser.utils import log, utils, javascript +from qutebrowser.utils import log, utils, javascript, usertypes from qutebrowser.browser import webelem +MYPY = False +if MYPY: + from qutebrowser.browser.webkit import webkittab class IsNullError(webelem.Error): @@ -36,7 +42,7 @@ class WebKitElement(webelem.AbstractWebElement): """A wrapper around a QWebElement.""" - def __init__(self, elem, tab): + def __init__(self, elem: QWebElement, tab: webkittab.WebKitTab) -> None: super().__init__(tab) if isinstance(elem, self.__class__): raise TypeError("Trying to wrap a wrapper!") @@ -44,90 +50,94 @@ class WebKitElement(webelem.AbstractWebElement): raise IsNullError('{} is a null element!'.format(elem)) self._elem = elem - def __str__(self): + def __str__(self) -> str: self._check_vanished() return self._elem.toPlainText() - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if not isinstance(other, WebKitElement): return NotImplemented return self._elem == other._elem # pylint: disable=protected-access - def __getitem__(self, key): + def __getitem__(self, key: str) -> str: self._check_vanished() if key not in self: raise KeyError(key) return self._elem.attribute(key) - def __setitem__(self, key, val): + def __setitem__(self, key: str, val: str) -> None: self._check_vanished() self._elem.setAttribute(key, val) - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: self._check_vanished() if key not in self: raise KeyError(key) self._elem.removeAttribute(key) - def __contains__(self, key): + def __contains__(self, key: object) -> bool: + assert isinstance(key, str) self._check_vanished() return self._elem.hasAttribute(key) - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: self._check_vanished() yield from self._elem.attributeNames() - def __len__(self): + def __len__(self) -> int: self._check_vanished() return len(self._elem.attributeNames()) - def _check_vanished(self): + def _check_vanished(self) -> None: """Raise an exception if the element vanished (is null).""" if self._elem.isNull(): raise IsNullError('Element {} vanished!'.format(self._elem)) - def has_frame(self): + def has_frame(self) -> bool: self._check_vanished() return self._elem.webFrame() is not None - def geometry(self): + def geometry(self) -> QRect: self._check_vanished() return self._elem.geometry() - def classes(self): + def classes(self) -> typing.List[str]: self._check_vanished() return self._elem.classes() - def tag_name(self): + def tag_name(self) -> str: """Get the tag name for the current element.""" self._check_vanished() return self._elem.tagName().lower() - def outer_xml(self): + def outer_xml(self) -> str: """Get the full HTML representation of this element.""" self._check_vanished() return self._elem.toOuterXml() - def value(self): + def value(self) -> webelem.JsValueType: self._check_vanished() val = self._elem.evaluateJavaScript('this.value') assert isinstance(val, (int, float, str, type(None))), val return val - def set_value(self, value): + def set_value(self, value: webelem.JsValueType) -> None: self._check_vanished() if self._tab.is_deleted(): raise webelem.OrphanedError("Tab containing element vanished") if self.is_content_editable(): log.webelem.debug("Filling {!r} via set_text.".format(self)) + assert isinstance(value, str) self._elem.setPlainText(value) else: log.webelem.debug("Filling {!r} via javascript.".format(self)) value = javascript.to_js(value) self._elem.evaluateJavaScript("this.value={}".format(value)) - def dispatch_event(self, event, bubbles=False, - cancelable=False, composed=False): + def dispatch_event(self, event: str, + bubbles: bool = False, + cancelable: bool = False, + composed: bool = False) -> None: self._check_vanished() log.webelem.debug("Firing event on {!r} via javascript.".format(self)) self._elem.evaluateJavaScript( @@ -138,7 +148,7 @@ class WebKitElement(webelem.AbstractWebElement): javascript.to_js(cancelable), javascript.to_js(composed))) - def caret_position(self): + def caret_position(self) -> int: """Get the text caret position for the current element.""" self._check_vanished() pos = self._elem.evaluateJavaScript('this.selectionStart') @@ -146,7 +156,7 @@ class WebKitElement(webelem.AbstractWebElement): return 0 return int(pos) - def insert_text(self, text): + def insert_text(self, text: str) -> None: self._check_vanished() if not self.is_editable(strict=True): raise webelem.Error("Element is not editable!") @@ -158,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement): this.dispatchEvent(event); """.format(javascript.to_js(text))) - def _parent(self): + def _parent(self) -> typing.Optional['WebKitElement']: """Get the parent element of this element.""" self._check_vanished() elem = self._elem.parent() @@ -166,7 +176,7 @@ class WebKitElement(webelem.AbstractWebElement): return None return WebKitElement(elem, tab=self._tab) - def _rect_on_view_js(self): + def _rect_on_view_js(self) -> typing.Optional[QRect]: """Javascript implementation for rect_on_view.""" # FIXME:qtwebengine maybe we can reuse this? rects = self._elem.evaluateJavaScript("this.getClientRects()") @@ -178,8 +188,8 @@ class WebKitElement(webelem.AbstractWebElement): return None text = utils.compact_text(self._elem.toOuterXml(), 500) - log.webelem.vdebug("Client rectangles of element '{}': {}".format( - text, rects)) + log.webelem.vdebug( # type: ignore + "Client rectangles of element '{}': {}".format(text, rects)) for i in range(int(rects.get("length", 0))): rect = rects[str(i)] @@ -204,7 +214,8 @@ class WebKitElement(webelem.AbstractWebElement): return None - def _rect_on_view_python(self, elem_geometry): + def _rect_on_view_python(self, + elem_geometry: typing.Optional[QRect]) -> QRect: """Python implementation for rect_on_view.""" if elem_geometry is None: geometry = self._elem.geometry() @@ -218,7 +229,8 @@ class WebKitElement(webelem.AbstractWebElement): frame = frame.parentFrame() return rect - def rect_on_view(self, *, elem_geometry=None, no_js=False): + def rect_on_view(self, *, elem_geometry: QRect = None, + no_js: bool = False) -> QRect: """Get the geometry of the element relative to the webview. Uses the getClientRects() JavaScript method to obtain the collection of @@ -248,7 +260,7 @@ class WebKitElement(webelem.AbstractWebElement): # No suitable rects found via JS, try via the QWebElement API return self._rect_on_view_python(elem_geometry) - def _is_visible(self, mainframe): + def _is_visible(self, mainframe: QWebFrame) -> bool: """Check if the given element is visible in the given frame. This is not public API because it can't be implemented easily here with @@ -300,8 +312,8 @@ class WebKitElement(webelem.AbstractWebElement): visible_in_frame = visible_on_screen return all([visible_on_screen, visible_in_frame]) - def remove_blank_target(self): - elem = self + def remove_blank_target(self) -> None: + elem = self # type: typing.Optional[WebKitElement] for _ in range(5): if elem is None: break @@ -311,14 +323,14 @@ class WebKitElement(webelem.AbstractWebElement): break elem = elem._parent() # pylint: disable=protected-access - def _move_text_cursor(self): + def _move_text_cursor(self) -> None: if self.is_text_input() and self.is_editable(): self._tab.caret.move_to_end_of_document() - def _requires_user_interaction(self): + def _requires_user_interaction(self) -> bool: return False - def _click_editable(self, click_target): + def _click_editable(self, click_target: usertypes.ClickTarget) -> None: ok = self._elem.evaluateJavaScript('this.focus(); true;') if ok: self._move_text_cursor() @@ -326,7 +338,7 @@ class WebKitElement(webelem.AbstractWebElement): log.webelem.debug("Failed to focus via JS, falling back to event") self._click_fake_event(click_target) - def _click_js(self, click_target): + def _click_js(self, click_target: usertypes.ClickTarget) -> None: settings = QWebSettings.globalSettings() attribute = QWebSettings.JavascriptCanOpenWindows could_open_windows = settings.testAttribute(attribute) @@ -337,12 +349,12 @@ class WebKitElement(webelem.AbstractWebElement): log.webelem.debug("Failed to click via JS, falling back to event") self._click_fake_event(click_target) - def _click_fake_event(self, click_target): + def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None: self._tab.data.override_target = click_target super()._click_fake_event(click_target) -def get_child_frames(startframe): +def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]: """Get all children recursively of a given QWebFrame. Loosely based on http://blog.nextgenetics.net/?e=64 @@ -356,7 +368,7 @@ def get_child_frames(startframe): results = [] frames = [startframe] while frames: - new_frames = [] + new_frames = [] # type: typing.List[QWebFrame] for frame in frames: results.append(frame) new_frames += frame.childFrames() From 02596fd325c853330f2a9cf5c223e5dc8b9f6c69 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Dec 2018 11:27:16 +0100 Subject: [PATCH 068/117] Fix docstring --- qutebrowser/browser/webelem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 9ae05639e..ac46fdcb9 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -62,11 +62,7 @@ def css_selector(group: str, url: QUrl) -> str: class AbstractWebElement(collections.abc.MutableMapping): - """A wrapper around QtWebKit/QtWebEngine web element. - - Attributes: - tab: The tab associated with this element. - """ + """A wrapper around QtWebKit/QtWebEngine web element.""" def __init__(self, tab: 'browsertab.AbstractTab') -> None: self._tab = tab From f49384f0bfc8ccf2639cd6c26b35c6215b64a55e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Dec 2018 19:44:48 +0100 Subject: [PATCH 069/117] Expose InterceptorType via qutebrowser.api.interceptor --- qutebrowser/api/interceptor.py | 6 +++++- qutebrowser/extensions/interceptors.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/qutebrowser/api/interceptor.py b/qutebrowser/api/interceptor.py index 634ae1409..78819dc46 100644 --- a/qutebrowser/api/interceptor.py +++ b/qutebrowser/api/interceptor.py @@ -24,7 +24,11 @@ from qutebrowser.extensions import interceptors from qutebrowser.extensions.interceptors import Request -def register(interceptor: interceptors.InterceptorType) -> None: +#: Type annotation for an interceptor function. +InterceptorType = interceptors.InterceptorType + + +def register(interceptor: InterceptorType) -> None: """Register a request interceptor. Whenever a request happens, the interceptor gets called with a diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py index 269c82ab8..7defcf213 100644 --- a/qutebrowser/extensions/interceptors.py +++ b/qutebrowser/extensions/interceptors.py @@ -47,6 +47,7 @@ class Request: self.is_blocked = True +#: Type annotation for an interceptor function. InterceptorType = typing.Callable[[Request], None] From 206a2f199b6300f1aee27577e23ab05b639cd16f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 14 Dec 2018 15:27:55 +0100 Subject: [PATCH 070/117] Fix annotation --- qutebrowser/browser/webkit/webkitelem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 44cc8ae2d..773bca67f 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -42,7 +42,7 @@ class WebKitElement(webelem.AbstractWebElement): """A wrapper around a QWebElement.""" - def __init__(self, elem: QWebElement, tab: webkittab.WebKitTab) -> None: + def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None: super().__init__(tab) if isinstance(elem, self.__class__): raise TypeError("Trying to wrap a wrapper!") From ba940f7f87155b9e669755ff101c4bea9127e594 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 15 Dec 2018 15:43:44 +0100 Subject: [PATCH 071/117] Disable Wayland check on Qt >= 5.11.2 Fixes #2932 --- doc/changelog.asciidoc | 2 ++ qutebrowser/misc/backendproblem.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index e4da4de4c..514261953 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -51,6 +51,8 @@ Changed adblocker can be disabled on a given page. - Elements with a `tabindex` attribute now also get hints by default. - Various small performance improvements for hints and the completion. +- The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those + versions should work without any issues. Fixed ~~~~~ diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 176746759..74a2ad372 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -238,6 +238,9 @@ def _handle_wayland(): if has_qt511 and config.val.qt.force_software_rendering == 'chromium': return + if qtutils.version_check('5.11.2', compiled=False): + return + buttons = [] text = "

You can work around this in one of the following ways:

" From 4da680f41c95c5982520ba58b7a258bbac8b357a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 15 Dec 2018 16:50:15 +0100 Subject: [PATCH 072/117] Fix lint/test issue --- qutebrowser/browser/webkit/webkitelem.py | 1 + tests/unit/browser/webkit/test_webkitelem.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 773bca67f..af0db295d 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -30,6 +30,7 @@ from qutebrowser.utils import log, utils, javascript, usertypes from qutebrowser.browser import webelem MYPY = False if MYPY: + # pylint: disable=unused-import,useless-suppression from qutebrowser.browser.webkit import webkittab diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 09be16848..17eae9c09 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -247,7 +247,7 @@ class TestWebKitElement: pytest.param(lambda e: e[None], id='getitem'), pytest.param(lambda e: operator.setitem(e, None, None), id='setitem'), pytest.param(lambda e: operator.delitem(e, None), id='delitem'), - pytest.param(lambda e: None in e, id='contains'), + pytest.param(lambda e: '' in e, id='contains'), pytest.param(list, id='iter'), pytest.param(len, id='len'), pytest.param(lambda e: e.has_frame(), id='has_frame'), From 01c25837d55ca3cba70a678f0e78b537c28c325b Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sat, 15 Dec 2018 23:38:25 +0700 Subject: [PATCH 073/117] Fix tab_mute command --- qutebrowser/components/misccommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 60715c65b..eaf45f40d 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -238,7 +238,7 @@ def tab_mute(tab: apitypes.Tab) -> None: if tab is None: return try: - tab.audio.set_muted(tab.audio.is_muted(), override=True) + tab.audio.set_muted(not tab.audio.is_muted(), override=True) except apitypes.WebTabError as e: raise cmdutils.CommandError(e) From a49f82062bdbf812c4640d018b6b5205e6542435 Mon Sep 17 00:00:00 2001 From: Hummer12007 Date: Fri, 21 Dec 2018 02:39:31 +0200 Subject: [PATCH 074/117] Use native style for downloads view on macOS --- qutebrowser/browser/downloadview.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index e90e37509..1ea2b6744 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -75,7 +75,8 @@ class DownloadView(QListView): def __init__(self, win_id, parent=None): super().__init__(parent) - self.setStyle(QStyleFactory.create('Fusion')) + if not utils.is_mac: + self.setStyle(QStyleFactory.create('Fusion')) config.set_register_stylesheet(self) self.setResizeMode(QListView.Adjust) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) From ad2add23951b46a044a9799c2c2597fc20ef25e3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:13 +0100 Subject: [PATCH 075/117] Update typed-ast from 1.1.0 to 1.1.1 --- misc/requirements/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index f84b63e92..6b8c63e97 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -5,4 +5,4 @@ mypy-extensions==0.4.1 PyQt5==5.11.3 PyQt5-sip==4.19.13 -e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs -typed-ast==1.1.0 +typed-ast==1.1.1 From cb688f9bba6e1a4e1d253f45ed51686d3cf887e5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:14 +0100 Subject: [PATCH 076/117] Update setuptools from 40.6.2 to 40.6.3 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 1e1934e76..f15a3a3e1 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==18.0 pyparsing==2.3.0 -setuptools==40.6.2 +setuptools==40.6.3 six==1.12.0 wheel==0.32.3 From 56a6d2dc547414a7819b6ef34229b150657d2e05 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:16 +0100 Subject: [PATCH 077/117] Update pygments from 2.3.0 to 2.3.1 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index c0801a48e..6f106798b 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -10,7 +10,7 @@ imagesize==1.1.0 Jinja2==2.10 MarkupSafe==1.1.0 packaging==18.0 -Pygments==2.3.0 +Pygments==2.3.1 pyparsing==2.3.0 pytz==2018.7 requests==2.21.0 From 3e70aef4482f54ddda2d821a74728f4134177a9d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:17 +0100 Subject: [PATCH 078/117] Update pygments from 2.3.0 to 2.3.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 66dcf23ef..68bd341bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ colorama==0.4.1 cssutils==1.0.2 Jinja2==2.10 MarkupSafe==1.1.0 -Pygments==2.3.0 +Pygments==2.3.1 pyPEG2==2.15.2 PyYAML==3.13 From f024b5b25a4059d2a29cf1a2e61fb8d6a6ff29c7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:19 +0100 Subject: [PATCH 079/117] Update easyprocess from 0.2.3 to 0.2.5 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 9b4bcc96f..439ce3f1e 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -8,7 +8,7 @@ cheroot==6.5.2 Click==7.0 # colorama==0.4.1 coverage==4.5.2 -EasyProcess==0.2.3 +EasyProcess==0.2.5 Flask==1.0.2 glob2==0.6 hunter==2.1.0 From 77034205a32f547fe38ed97598a66e28e7b9f10b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:20 +0100 Subject: [PATCH 080/117] Update hypothesis from 3.82.5 to 3.84.5 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 439ce3f1e..394f69bb4 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -12,7 +12,7 @@ EasyProcess==0.2.5 Flask==1.0.2 glob2==0.6 hunter==2.1.0 -hypothesis==3.82.5 +hypothesis==3.84.5 itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 From f45acd0361fa3c025e7cf0203bcba8fc117c4c0b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:22 +0100 Subject: [PATCH 081/117] Update pytest from 4.0.1 to 4.0.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 394f69bb4..739987d04 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -23,7 +23,7 @@ parse-type==0.4.2 pluggy==0.8.0 py==1.7.0 py-cpuinfo==4.0.0 -pytest==4.0.1 +pytest==4.0.2 pytest-bdd==3.0.0 pytest-benchmark==3.1.1 pytest-cov==2.6.0 From e529dbbc84c93f0bc325c93f2b64aaa1e3b69920 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:23 +0100 Subject: [PATCH 082/117] Update pytest-bdd from 3.0.0 to 3.0.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 739987d04..10f12b90f 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -24,7 +24,7 @@ pluggy==0.8.0 py==1.7.0 py-cpuinfo==4.0.0 pytest==4.0.2 -pytest-bdd==3.0.0 +pytest-bdd==3.0.1 pytest-benchmark==3.1.1 pytest-cov==2.6.0 pytest-faulthandler==1.5.0 From 4968c6bd760216011d50a87dbf8ef845c69e8692 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:25 +0100 Subject: [PATCH 083/117] Update pytest-qt from 3.2.1 to 3.2.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 10f12b90f..28c26d891 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -30,7 +30,7 @@ pytest-cov==2.6.0 pytest-faulthandler==1.5.0 pytest-instafail==0.4.0 pytest-mock==1.10.0 -pytest-qt==3.2.1 +pytest-qt==3.2.2 pytest-repeat==0.7.0 pytest-rerunfailures==5.0 pytest-travis-fold==1.3.0 From 4cc9d7f68abfae64061c41b2eb47ea4834b1e6e7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 24 Dec 2018 19:21:26 +0100 Subject: [PATCH 084/117] Update tox from 3.5.3 to 3.6.1 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index b6c9e9d6f..ed0db2870 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -5,5 +5,5 @@ pluggy==0.8.0 py==1.7.0 six==1.12.0 toml==0.10.0 -tox==3.5.3 +tox==3.6.1 virtualenv==16.1.0 From 49d468527a6a54769d4d6a0c78945e2063ada1aa Mon Sep 17 00:00:00 2001 From: ykgmfq Date: Sun, 30 Dec 2018 14:58:02 +0100 Subject: [PATCH 085/117] Remove obsolete Fedora instructions --- doc/install.asciidoc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/install.asciidoc b/doc/install.asciidoc index 37f6ebb12..c9ae54cd2 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -102,18 +102,12 @@ $ python3 scripts/asciidoc2html.py On Fedora --------- -NOTE: Fedora's packages used to be outdated for a long time, but are -now (November 2017) maintained and up-to-date again. - qutebrowser is available in the official repositories: ----- # dnf install qutebrowser ----- -However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you -might want to <> instead there. - Additional hints ~~~~~~~~~~~~~~~~ From dc2462508483974ec0bcab578114640bf0995ba6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:13 +0100 Subject: [PATCH 086/117] Update flake8-per-file-ignores from 0.6 to 0.7 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 20a66cd5f..42255f825 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -11,7 +11,7 @@ flake8-deprecated==1.3 flake8-docstrings==1.3.0 flake8-future-import==0.4.5 flake8-mock==0.3 -flake8-per-file-ignores==0.6 +flake8-per-file-ignores==0.7 flake8-polyfill==1.0.2 flake8-string-format==0.2.3 flake8-tidy-imports==1.1.0 From 53f53caa53ab3bf4975d670dd6a61e192a53fa5a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:15 +0100 Subject: [PATCH 087/117] Update sphinx from 1.8.2 to 1.8.3 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 6f106798b..c089895d1 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -16,6 +16,6 @@ pytz==2018.7 requests==2.21.0 six==1.12.0 snowballstemmer==1.2.1 -Sphinx==1.8.2 +Sphinx==1.8.3 sphinxcontrib-websupport==1.1.0 urllib3==1.24.1 From f90d5f13d27cc607d7ab0c0e4e1e2c11645e255b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:16 +0100 Subject: [PATCH 088/117] Update beautifulsoup4 from 4.6.3 to 4.7.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 28c26d891..a9e3f76b1 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -3,7 +3,7 @@ atomicwrites==1.2.1 attrs==18.2.0 backports.functools-lru-cache==1.5 -beautifulsoup4==4.6.3 +beautifulsoup4==4.7.0 cheroot==6.5.2 Click==7.0 # colorama==0.4.1 From a8e7f3ca6b5454712257de5750d436419aedd729 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:18 +0100 Subject: [PATCH 089/117] Update cheroot from 6.5.2 to 6.5.3 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index a9e3f76b1..cbd2ea53c 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -4,7 +4,7 @@ atomicwrites==1.2.1 attrs==18.2.0 backports.functools-lru-cache==1.5 beautifulsoup4==4.7.0 -cheroot==6.5.2 +cheroot==6.5.3 Click==7.0 # colorama==0.4.1 coverage==4.5.2 From 590203b2a19b61fa9f70c2f490ec554180a552f9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:19 +0100 Subject: [PATCH 090/117] Update hypothesis from 3.84.5 to 3.85.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index cbd2ea53c..d2a9f0518 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -12,7 +12,7 @@ EasyProcess==0.2.5 Flask==1.0.2 glob2==0.6 hunter==2.1.0 -hypothesis==3.84.5 +hypothesis==3.85.2 itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 From 5b94208389f0e9d4497b78680882f63e2948e81b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 31 Dec 2018 19:23:20 +0100 Subject: [PATCH 091/117] Update more-itertools from 4.3.0 to 5.0.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index d2a9f0518..228045f8c 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -17,7 +17,7 @@ itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 # MarkupSafe==1.1.0 -more-itertools==4.3.0 +more-itertools==5.0.0 parse==1.9.0 parse-type==0.4.2 pluggy==0.8.0 From f9be35a30aef27b6c767404017150e13ee31ddd7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 2 Jan 2019 23:23:28 +0100 Subject: [PATCH 092/117] Add pywal to docs --- doc/help/configuring.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 33711b755..b04c9e9df 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -396,6 +396,7 @@ Pre-built colorschemes - A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager]. - Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon] - https://github.com/evannagle/qutebrowser-dracula-theme[Dracula] +- https://github.com/jjzmajic/qutewal[Pywal theme] Avoiding flake8 errors ^^^^^^^^^^^^^^^^^^^^^^ From 1ef793fa6617c434a0ba97caed12014d50a4b534 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Jan 2019 13:40:05 +0100 Subject: [PATCH 093/117] Qt 5.12: Disable chrome-error:// and chrome-extension:// workaround --- qutebrowser/browser/webengine/webenginequtescheme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 68ffa019d..4e42e0eb5 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -34,7 +34,8 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): def install(self, profile): """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) - if qtutils.version_check('5.11', compiled=False): + if (qtutils.version_check('5.11', compiled=False) and + not qtutils.version_check('5.12', compiled=False)): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 profile.installUrlSchemeHandler(b'chrome-error', self) profile.installUrlSchemeHandler(b'chrome-extension', self) From 530befd68154440348c776a7dbf3683a7d90d9d1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Jan 2019 14:38:12 +0100 Subject: [PATCH 094/117] Register qute:// scheme with Qt See #3992, #4198 --- .../browser/webengine/webenginequtescheme.py | 21 +++++++++++++++++++ .../browser/webengine/webenginesettings.py | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 4e42e0eb5..816589514 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -22,6 +22,11 @@ from PyQt5.QtCore import QBuffer, QIODevice, QUrl from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob) +try: + from PyQt5.QtWebEngineCore import QWebEngineUrlScheme # type: ignore +except ImportError: + # Added in Qt 5.12 + QWebEngineUrlScheme = None from qutebrowser.browser import qutescheme from qutebrowser.utils import log, qtutils @@ -33,6 +38,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): def install(self, profile): """Install the handler for qute:// URLs on the given profile.""" + if QWebEngineUrlScheme is not None: + assert QWebEngineUrlScheme.schemeByName(b'qute') is not None + profile.installUrlSchemeHandler(b'qute', self) if (qtutils.version_check('5.11', compiled=False) and not qtutils.version_check('5.12', compiled=False)): @@ -131,3 +139,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): buf.seek(0) buf.close() job.reply(mimetype.encode('ascii'), buf) + + +def init(): + """Register the qute:// scheme. + + Note this needs to be called early, before constructing any QtWebEngine + classes. + """ + if QWebEngineUrlScheme is not None: + scheme = QWebEngineUrlScheme(b'qute') + scheme.setFlags(QWebEngineUrlScheme.LocalScheme | + QWebEngineUrlScheme.LocalAccessAllowed) + QWebEngineUrlScheme.registerScheme(scheme) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index da569eef6..10c4d4e6b 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, QWebEnginePage) -from qutebrowser.browser.webengine import spell +from qutebrowser.browser.webengine import spell, webenginequtescheme from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr from qutebrowser.utils import utils, standarddir, qtutils, message, log @@ -298,6 +298,7 @@ def init(args): not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11 os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) + webenginequtescheme.init() spell.init() _init_profiles() From 060489f1bc5b5a6529bd8e78d80449a4b06dd568 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 3 Jan 2019 22:11:58 +0100 Subject: [PATCH 095/117] Fix new shellcheck issues --- misc/userscripts/qutedmenu | 4 ++-- scripts/dev/ci/travis_install.sh | 2 +- scripts/dev/ci/travis_run.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu index de1b8d641..cc5a44413 100755 --- a/misc/userscripts/qutedmenu +++ b/misc/userscripts/qutedmenu @@ -37,7 +37,7 @@ get_selection() { # https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font [[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font -[[ $font ]] && opts+=(-fn "$font") +[[ -n $font ]] && opts+=(-fn "$font") # shellcheck source=/dev/null [[ -s $optsfile ]] && source "$optsfile" @@ -46,7 +46,7 @@ url=$(get_selection) url=${url/*http/http} # If no selection is made, exit (escape pressed, e.g.) -[[ ! $url ]] && exit 0 +[[ -z $url ]] && exit 0 case $1 in open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;; diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index 18f5aa9ec..c736a01d3 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -71,7 +71,7 @@ EOF set -e -if [[ $DOCKER ]]; then +if [[ -n $DOCKER ]]; then exit 0 elif [[ $TRAVIS_OS_NAME == osx ]]; then # Disable App Nap diff --git a/scripts/dev/ci/travis_run.sh b/scripts/dev/ci/travis_run.sh index a287e844e..4e338221f 100644 --- a/scripts/dev/ci/travis_run.sh +++ b/scripts/dev/ci/travis_run.sh @@ -1,6 +1,6 @@ #!/bin/bash -if [[ $DOCKER ]]; then +if [[ -n $DOCKER ]]; then docker run \ --privileged \ -v "$PWD:/outside" \ From 3528de82e9944c853c6d76eb1823a8eb9b8e4488 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 4 Jan 2019 10:30:06 +0100 Subject: [PATCH 096/117] Also skip SSL test on Qt 5.12 See #4240 --- tests/end2end/features/downloads.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 7bde58b4f..b1d92145d 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -93,7 +93,7 @@ Feature: Downloading things from a website. Then no crash should happen # https://github.com/qutebrowser/qutebrowser/issues/4240 - @qt!=5.11.2 + @qt<5.11.2 Scenario: Downloading with SSL errors (issue 1413) When SSL is supported And I clear SSL errors From 24a1104dc750659679f9440945e5dbe93a681058 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 4 Jan 2019 18:03:33 -0500 Subject: [PATCH 097/117] Implement pinned.tab.frozen setting (issue #4400) Implement a new setting, `pinned.tab.frozen` (boolean), which when false allows a user to navigate to new URLs in a pinned tab (default behavior is to have `pinned.tab.frozen = true`, in which no navigation is allowed). issue URL: https://github.com/qutebrowser/qutebrowser/issues/4400 Changes to be committed: modified: qutebrowser/browser/browsertab.py Add `navigation_blocked` method to AbstractTab, which returns true if navigation is allowed on a tab. modified: qutebrowser/browser/commands.py Change `openurl` to use `navigation_blocked` method of a tab to determine behavior while navigating a pinned tab. modified: qutebrowser/components/misccommands.py Change `home` to use `navigation_blocked` method of a tab to determine behavior while navigating a pinned tab. modified: qutebrowser/config/configdata.yml Add `pinned.tab.frozen` config. modified: tests/end2end/features/tabs.feature Add tests for :open and :home on pinned tabs with `pinned.tab.frozen` set to false --- qutebrowser/browser/browsertab.py | 4 ++++ qutebrowser/browser/commands.py | 7 ++++--- qutebrowser/components/misccommands.py | 2 +- qutebrowser/config/configdata.yml | 5 +++++ tests/end2end/features/tabs.feature | 18 ++++++++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 029394657..80871a254 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -943,6 +943,10 @@ class AbstractTab(QWidget): evt.posted = True QApplication.postEvent(recipient, evt) + def navigation_blocked(self) -> bool: + """Test if navigation is allowed on the current tab.""" + return self.data.pinned and config.val.pinned.tab.frozen + @pyqtSlot(QUrl) def _on_before_load_started(self, url: QUrl) -> None: """Adjust the title if we are going to visit a URL soon.""" diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 571e06d8b..c47811701 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -316,10 +316,11 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return - elif curtab.data.pinned: - message.info("Tab is pinned!") else: - curtab.load_url(cur_url) + if curtab.navigation_blocked(): + message.info("Tab is pinned!") + else: + curtab.load_url(cur_url) def _parse_url(self, url, *, force_search=False): """Parse a URL or quickmark or search query. diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index eaf45f40d..a65bdd235 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -118,7 +118,7 @@ def printpage(tab: apitypes.Tab, @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def home(tab: apitypes.Tab) -> None: """Open main startpage in current tab.""" - if tab.data.pinned: + if tab.navigation_blocked(): message.info("Tab is pinned!") else: tab.load_url(config.val.url.start_pages[0]) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index bed4d9659..15b3eb446 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2917,3 +2917,8 @@ bindings.commands: * register: Entered when qutebrowser is waiting for a register name/key for commands like `:set-mark`. + +pinned.tab.frozen: + type: Bool + default: True + desc: If true, then pinned tabs cannot have their URL changed. diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 7f4d4635c..ec6400458 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1289,6 +1289,14 @@ Feature: Tab management And the following tabs should be open: - data/numbers/1.txt (active) (pinned) + Scenario: :tab-pin open url with pinned.tab.frozen = false + When I set pinned.tab.frozen to false + And I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt + Then the following tabs should be open: + - data/numbers/2.txt (active) (pinned) + Scenario: :home on a pinned tab When I open data/numbers/1.txt And I run :tab-pin @@ -1297,6 +1305,16 @@ Feature: Tab management And the following tabs should be open: - data/numbers/1.txt (active) (pinned) + Scenario: :home on a pinned tab with pinned.tab.frozen = false + When I set url.start_pages to ["http://localhost:(port)/data/numbers/2.txt"] + And I set pinned.tab.frozen to false + And I open data/numbers/1.txt + And I run :tab-pin + And I run :home + Then data/numbers/2.txt should be loaded + And the following tabs should be open: + - data/numbers/2.txt (active) (pinned) + Scenario: Cloning a pinned tab When I open data/numbers/1.txt And I run :tab-pin From 9662d3aa252dc31c574c52299b9abc6560d6334c Mon Sep 17 00:00:00 2001 From: adam Date: Sat, 5 Jan 2019 23:21:59 -0500 Subject: [PATCH 098/117] Change commands.py to maintain old indent level Changes to be committed: modified: qutebrowser/browser/commands.py --- qutebrowser/browser/commands.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index c47811701..6ae47f9d9 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -316,11 +316,10 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return + elif curtab.navigation_blocked(): + message.info("Tab is pinned!") else: - if curtab.navigation_blocked(): - message.info("Tab is pinned!") - else: - curtab.load_url(cur_url) + curtab.load_url(cur_url) def _parse_url(self, url, *, force_search=False): """Parse a URL or quickmark or search query. From 5cd988ae52eeadb85d97bfa95c2773c0e3f4e7a8 Mon Sep 17 00:00:00 2001 From: adam Date: Sun, 6 Jan 2019 20:53:26 -0500 Subject: [PATCH 099/117] Change config option to in tabs.pinned group Changes to be committed: modified: qutebrowser/browser/browsertab.py modified: qutebrowser/config/configdata.yml modified: tests/end2end/features/tabs.feature Changed pinned.tabs.frozen to be tabs.pinned.frozen in all of these files. --- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/config/configdata.yml | 10 +++++----- tests/end2end/features/tabs.feature | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 80871a254..55ab89a20 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -945,7 +945,7 @@ class AbstractTab(QWidget): def navigation_blocked(self) -> bool: """Test if navigation is allowed on the current tab.""" - return self.data.pinned and config.val.pinned.tab.frozen + return self.data.pinned and config.val.tabs.pinned.frozen @pyqtSlot(QUrl) def _on_before_load_started(self, url: QUrl) -> None: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 15b3eb446..f6313cc88 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1768,6 +1768,11 @@ tabs.pinned.shrink: type: Bool desc: Shrink pinned tabs down to their contents. +tabs.pinned.frozen: + type: Bool + default: True + desc: If true, then pinned tabs cannot have their URL changed. + tabs.wrap: default: true type: Bool @@ -2917,8 +2922,3 @@ bindings.commands: * register: Entered when qutebrowser is waiting for a register name/key for commands like `:set-mark`. - -pinned.tab.frozen: - type: Bool - default: True - desc: If true, then pinned tabs cannot have their URL changed. diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index ec6400458..804f72bbf 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1289,8 +1289,8 @@ Feature: Tab management And the following tabs should be open: - data/numbers/1.txt (active) (pinned) - Scenario: :tab-pin open url with pinned.tab.frozen = false - When I set pinned.tab.frozen to false + Scenario: :tab-pin open url with tabs.pinned.frozen = false + When I set tabs.pinned.frozen to false And I open data/numbers/1.txt And I run :tab-pin And I open data/numbers/2.txt @@ -1305,9 +1305,9 @@ Feature: Tab management And the following tabs should be open: - data/numbers/1.txt (active) (pinned) - Scenario: :home on a pinned tab with pinned.tab.frozen = false + Scenario: :home on a pinned tab with tabs.pinned.frozen = false When I set url.start_pages to ["http://localhost:(port)/data/numbers/2.txt"] - And I set pinned.tab.frozen to false + And I set tabs.pinned.frozen to false And I open data/numbers/1.txt And I run :tab-pin And I run :home From 65ae7ca455b2520f5f78ec4ab497cdb073259602 Mon Sep 17 00:00:00 2001 From: adam Date: Mon, 7 Jan 2019 15:42:01 -0500 Subject: [PATCH 100/117] Make option desc imperative As given in commit https://github.com/qutebrowser/qutebrowser/commit/494aceec451e307b0227b0fdd0f86d419dbc11d1 Changes to be committed: modified: qutebrowser/config/configdata.yml --- qutebrowser/config/configdata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index f6313cc88..b6d4736ae 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1771,7 +1771,7 @@ tabs.pinned.shrink: tabs.pinned.frozen: type: Bool default: True - desc: If true, then pinned tabs cannot have their URL changed. + desc: Force pinned tabs to stay at fixed URL. tabs.wrap: default: true From 8a48e5d2a5357d84e31f8d14b748dd7b94fe44ff Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 Oct 2018 23:28:36 -0700 Subject: [PATCH 101/117] Add benchmark for config cache --- tests/unit/config/test_configcache.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/config/test_configcache.py b/tests/unit/config/test_configcache.py index 91e2f22fa..7c0f6012f 100644 --- a/tests/unit/config/test_configcache.py +++ b/tests/unit/config/test_configcache.py @@ -50,3 +50,17 @@ def test_configcache_get_after_set(config_stub): assert not config.cache['auto_save.session'] config_stub.val.auto_save.session = True assert config.cache['auto_save.session'] + + +def test_configcache_naive_benchmark(config_stub, benchmark): + def _run_bench(): + for _i in range(10000): + # pylint: disable=pointless-statement + config.cache['tabs.padding'] + config.cache['tabs.indicator.width'] + config.cache['tabs.indicator.padding'] + config.cache['tabs.min_width'] + config.cache['tabs.max_width'] + config.cache['tabs.pinned.shrink'] + # pylint: enable=pointless-statement + benchmark(_run_bench) From 8bb2db5e56fb1553287bb1eb07f49f37438c8562 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 Oct 2018 23:31:07 -0700 Subject: [PATCH 102/117] Restructure cache to optimize critical path --- qutebrowser/config/configcache.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py index a421ba85c..15f343478 100644 --- a/qutebrowser/config/configcache.py +++ b/qutebrowser/config/configcache.py @@ -46,7 +46,9 @@ class ConfigCache: self._cache[attr] = config.instance.get(attr) def __getitem__(self, attr: str) -> typing.Any: - if attr not in self._cache: + try: + return self._cache[attr] + except KeyError: assert not config.instance.get_opt(attr).supports_pattern self._cache[attr] = config.instance.get(attr) - return self._cache[attr] + return self._cache[attr] From 2c1d039c9e56ac3b643d11630c58a5085af86dd4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 11 Jan 2019 11:21:17 +0100 Subject: [PATCH 103/117] Refactor tests for bool setting migrations --- tests/unit/config/test_configfiles.py | 36 +++++++++------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 2b793b58b..cba7a4a7f 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -250,36 +250,24 @@ class TestYaml: data = autoconfig.read() assert data['content.webrtc_ip_handling_policy']['global'] == expected - @pytest.mark.parametrize('show, expected', [ - (True, 'always'), - (False, 'never'), - ('always', 'always'), - ('never', 'never'), - ('pinned', 'pinned'), + @pytest.mark.parametrize('setting, old, new', [ + ('tabs.favicons.show', True, 'always'), + ('tabs.favicons.show', False, 'never'), + ('tabs.favicons.show', 'always', 'always'), + + ('qt.force_software_rendering', True, 'software-opengl'), + ('qt.force_software_rendering', False, 'none'), + ('qt.force_software_rendering', 'chromium', 'chromium'), ]) - def test_tabs_favicons_show(self, yaml, autoconfig, show, expected): - """Tests for migration of tabs.favicons.show.""" - autoconfig.write({'tabs.favicons.show': {'global': show}}) + def test_bool_migrations(self, yaml, autoconfig, setting, old, new): + """Tests for migration of former boolean settings.""" + autoconfig.write({setting: {'global': old}}) yaml.load() yaml._save() data = autoconfig.read() - assert data['tabs.favicons.show']['global'] == expected - - @pytest.mark.parametrize('force, expected', [ - (True, 'software-opengl'), - (False, 'none'), - ('chromium', 'chromium'), - ]) - def test_force_software_rendering(self, yaml, autoconfig, force, expected): - autoconfig.write({'qt.force_software_rendering': {'global': force}}) - - yaml.load() - yaml._save() - - data = autoconfig.read() - assert data['qt.force_software_rendering']['global'] == expected + assert data[setting]['global'] == new def test_renamed_key_unknown_target(self, monkeypatch, yaml, autoconfig): From cc0f5fc6d400e12833ba729049e31d16cf836d53 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 11 Jan 2019 11:23:58 +0100 Subject: [PATCH 104/117] Fix migration of scrolling.bar --- doc/changelog.asciidoc | 2 ++ qutebrowser/config/configfiles.py | 2 +- tests/unit/config/test_configfiles.py | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 514261953..acf02f951 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -70,6 +70,8 @@ Fixed - `:navigate` not incrementing in anchors or queries or anchors. - Crash when trying to use a proxy requiring authentication with QtWebKit. - Slashes in search terms are now percent-escaped. +- When `scrolling.bar = True` was set in versions before v1.5.0, this now + correctly gets migrated to `always` instead of `when-searching`. v1.5.2 ------ diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 54ca91488..de50dfca9 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -308,7 +308,7 @@ class YamlConfig(QObject): self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never') self._migrate_bool(settings, 'scrolling.bar', - 'when-searching', 'never') + 'always', 'when-searching') self._migrate_bool(settings, 'qt.force_software_rendering', 'software-opengl', 'none') diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index cba7a4a7f..79d4b9d89 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -255,6 +255,10 @@ class TestYaml: ('tabs.favicons.show', False, 'never'), ('tabs.favicons.show', 'always', 'always'), + ('scrolling.bar', True, 'always'), + ('scrolling.bar', False, 'when-searching'), + ('scrolling.bar', 'always', 'always'), + ('qt.force_software_rendering', True, 'software-opengl'), ('qt.force_software_rendering', False, 'none'), ('qt.force_software_rendering', 'chromium', 'chromium'), From 322b4d26d8b650340550871d0a1e5e75f817c63a Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Sat, 12 Jan 2019 10:10:05 -0800 Subject: [PATCH 105/117] Fix match highlighting on qt 5.11.3 and 5.12.1+ For some reason, populating the text (even if we completely overwrite it later) before the highlighter is added results in no highlighting. I think this has something to do with python locking/delayed re-highlighting. At some point, it would be nice to file this as a bug, but I can't seem to make a small c++ example. See https://codereview.qt-project.org/#/c/244699/4//ALL and https://bugreports.qt.io/browse/QTBUG-71307 for more information. Closes #4519 --- qutebrowser/completion/completiondelegate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 3ed5f0a64..b58f36372 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -212,11 +212,11 @@ class CompletionItemDelegate(QStyledItemDelegate): view = self.parent() pattern = view.pattern columns_to_filter = index.model().columns_to_filter(index) - self._doc.setPlainText(self._opt.text) if index.column() in columns_to_filter and pattern: pat = re.escape(pattern).replace(r'\ ', r'|') _Highlighter(self._doc, pat, config.val.colors.completion.match.fg) + self._doc.setPlainText(self._opt.text) else: self._doc.setHtml( '{}'.format( From d2bac602c22804cede55508076c2c85e4680796a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 10:36:02 +0100 Subject: [PATCH 106/117] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index acf02f951..ea04a8262 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -72,6 +72,7 @@ Fixed - Slashes in search terms are now percent-escaped. - When `scrolling.bar = True` was set in versions before v1.5.0, this now correctly gets migrated to `always` instead of `when-searching`. +- Completion highlighting now works again on Qt 5.11.3 and 5.12.1. v1.5.2 ------ From d8c02f495e0da39d1a11c340f6dc23281988f255 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 10:36:14 +0100 Subject: [PATCH 107/117] Add highlighting test --- .../completion/test_completiondelegate.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 2d122927d..7d310380e 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -20,7 +20,8 @@ from unittest import mock import pytest from PyQt5.QtCore import Qt -from PyQt5.QtGui import QTextDocument +from PyQt5.QtGui import QTextDocument, QColor +from PyQt5.QtWidgets import QTextEdit from qutebrowser.completion import completiondelegate @@ -50,3 +51,24 @@ def test_highlight(pat, txt, segments): highlighter.setFormat.assert_has_calls([ mock.call(s[0], s[1], mock.ANY) for s in segments ]) + + +def test_highlighted(qtbot): + """Make sure highlighting works. + + Note that with Qt 5.11.3 and > 5.12.1 we need to call setPlainText *after* + creating the highlighter for highlighting to work. Ideally, we'd test + whether CompletionItemDelegate._get_textdoc() works properly, but testing + that is kind of hard, so we just test it in isolation here. + """ + doc = QTextDocument() + completiondelegate._Highlighter(doc, 'Hello', Qt.red) + doc.setPlainText('Hello World') + + # Needed so the highlighting actually works. + edit = QTextEdit() + qtbot.addWidget(edit) + edit.setDocument(doc) + + colors = [f.foreground().color() for f in doc.allFormats()] + assert QColor('red') in colors From 3fc545381cbd9a6db69c373bcc9e1aa792699f62 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 10:38:05 +0100 Subject: [PATCH 108/117] Ignore new Qt warning --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index c907173e0..c278b0591 100644 --- a/pytest.ini +++ b/pytest.ini @@ -64,6 +64,7 @@ qt_log_ignore = ^QSettings::value: Empty key passed ^Icon theme ".*" not found ^Error receiving trust for a CA certificate + ^QBackingStore::endPaint\(\) called with active painter on backingstore paint device xfail_strict = true filterwarnings = error From 5790857b95e47e6678d1cfe2607104590d98a040 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 10:40:27 +0100 Subject: [PATCH 109/117] Make shellcheck happy --- misc/userscripts/password_fill | 2 ++ 1 file changed, 2 insertions(+) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 2e4f01b66..7d4be0467 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -64,6 +64,7 @@ die() { javascript_escape() { # print the first argument in an escaped way, such that it can safely # be used within javascripts double quotes + # shellcheck disable=SC2001 sed "s,[\\\\'\"],\\\\&,g" <<< "$1" } @@ -111,6 +112,7 @@ simplify_url() { # are found: no_entries_found() { while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do + # shellcheck disable=SC2001 shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") if [ "$shorter_simple_url" = "$simple_url" ] ; then # if no dot, then even remove the top level domain From 9d05f412d725e16e46fb36287622d5038649be36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 11:49:02 +0100 Subject: [PATCH 110/117] Update docs --- doc/help/settings.asciidoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 7da6e543e..eb3907cce 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -261,6 +261,7 @@ |<>|Stack related tabs on top of each other when opened consecutively. |<>|Position of new tabs which are not opened from another tab. |<>|Padding (in pixels) around text for tabs. +|<>|Force pinned tabs to stay at fixed URL. |<>|Shrink pinned tabs down to their contents. |<>|Position of the tab bar. |<>|Which tab to select when the focused tab is removed. @@ -3307,6 +3308,14 @@ Default: - +pass:[right]+: +pass:[5]+ - +pass:[top]+: +pass:[0]+ +[[tabs.pinned.frozen]] +=== tabs.pinned.frozen +Force pinned tabs to stay at fixed URL. + +Type: <> + +Default: +pass:[true]+ + [[tabs.pinned.shrink]] === tabs.pinned.shrink Shrink pinned tabs down to their contents. From 824bf8d5b6b1ee2dfbd61963f62c1a898d11a4fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 11:50:20 +0100 Subject: [PATCH 111/117] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index ea04a8262..ceab9d043 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -25,6 +25,7 @@ Added opened from a page should stack on each other or not. - New `completion.open_categories` setting which allows to configure which categories are shown in the `:open` completion, and how they are ordered. +- New `tabs.pinned.frozen` setting to allow/deny navigating in pinned tabs. - New config manipulation commands: * `:config-dict-add` and `:config-list-add` to a new element to a dict/list setting. From 18f0034f3cb78d901475d71e46a8f5e4d8efd76a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Jan 2019 13:35:50 +0100 Subject: [PATCH 112/117] travis: Disable notifications Looks like Notifico takes care of that, too. --- .travis.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 663c4c6f9..dfa566671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,16 +68,3 @@ after_success: after_failure: - bash scripts/dev/ci/travis_backtrace.sh - -notifications: - webhooks: - - https://buildtimetrend.herokuapp.com/travis - irc: - channels: - - "chat.freenode.net#qutebrowser-dev" - on_success: always - on_failure: always - skip_join: true - template: - - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}" - - "%{compare_url} - %{build_url}" From b4d8e3ff5dbdc12abac91eaf8e6f99ead9de91d8 Mon Sep 17 00:00:00 2001 From: Nat West Date: Mon, 14 Jan 2019 01:10:12 +0000 Subject: [PATCH 113/117] Update qutebrowser.org/doc/faq.html question 'What's the difference between insert and passthrough mode?' to reflect escape->shift+escape keybinding change --- doc/faq.asciidoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 113a11f09..10661cb9e 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -211,9 +211,10 @@ Why does J move to the next (right) tab, and K to the previous (left) one?:: What's the difference between insert and passthrough mode?:: They are quite similar, but insert mode has some bindings (like `Ctrl-e` to - open an editor) while passthrough mode only has escape bound. It might also - be useful to rebind escape to something else in passthrough mode only, to be - able to send an escape keypress to the website. + open an editor) while passthrough mode only has shift+escape bound. This is + because shift+escape is unlikely to be a useful binding to be passed to a + webpage. However, any other keys may be assigned to leaving passthrough mode + instead of shift+escape should this be desired. Why does it take longer to open a URL in qutebrowser than in chromium?:: When opening a URL in an existing instance, the normal qutebrowser From 1799b7926a0202497a88e4ee1fdb232f06ab8e3a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 14 Jan 2019 22:22:56 +0100 Subject: [PATCH 114/117] Make console available in PAC files --- doc/changelog.asciidoc | 1 + qutebrowser/browser/network/pac.py | 2 ++ tests/unit/browser/webkit/network/test_pac.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index ceab9d043..a116b53e6 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -54,6 +54,7 @@ Changed - Various small performance improvements for hints and the completion. - The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those versions should work without any issues. +- The JavaScript `console` object is now available in PAC files. Fixed ~~~~~ diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 1c6075945..bd060820b 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -180,6 +180,8 @@ class PACResolver: """ self._engine = QJSEngine() + self._engine.installExtensions(QJSEngine.ConsoleExtension) + self._ctx = _PACContext(self._engine) self._engine.globalObject().setProperty( "PAC", self._engine.newQObject(self._ctx)) diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py index 8c03c6cee..5aebecbf2 100644 --- a/tests/unit/browser/webkit/network/test_pac.py +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -205,6 +205,20 @@ def test_secret_url(url, has_secret, from_file): res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file) +def test_logging(qtlog): + """Make sure console.log() works for PAC files.""" + test_str = """ + function FindProxyForURL(domain, host) { + console.log("logging test"); + return "DIRECT"; + } + """ + res = pac.PACResolver(test_str) + res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test"))) + assert len(qtlog.records) == 1 + assert qtlog.records[0].message == 'logging test' + + def fetcher_test(test_str): class PACHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): From b415edaf62680facec826b4457f8318392de9125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20M=2E=20P=C3=B6pperl?= Date: Sun, 20 Jan 2019 19:52:38 +0100 Subject: [PATCH 115/117] Rename appdata.xml to reflect id string This helps for Flatpak packaging. See also 30f7f7b --- MANIFEST.in | 2 +- misc/Makefile | 4 ++-- ...er.appdata.xml => org.qutebrowser.qutebrowser.appdata.xml} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename misc/{qutebrowser.appdata.xml => org.qutebrowser.qutebrowser.appdata.xml} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index cd9e50cf9..a0e61c30e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,7 +13,7 @@ include qutebrowser/utils/testfile include qutebrowser/git-commit-id include LICENSE doc/* README.asciidoc include misc/qutebrowser.desktop -include misc/qutebrowser.appdata.xml +include misc/org.qutebrowser.qutebrowser.appdata.xml include misc/Makefile include requirements.txt include tox.ini diff --git a/misc/Makefile b/misc/Makefile index 4625b288e..526f7adce 100644 --- a/misc/Makefile +++ b/misc/Makefile @@ -17,8 +17,8 @@ doc/qutebrowser.1.html: install: doc/qutebrowser.1.html $(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS) - install -Dm644 misc/qutebrowser.appdata.xml \ - "$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml" + install -Dm644 misc/org.qutebrowser.qutebrowser.appdata.xml \ + "$(DESTDIR)$(DATADIR)/metainfo/org.qutebrowser.qutebrowser.appdata.xml" install -Dm644 doc/qutebrowser.1 \ "$(DESTDIR)$(MANDIR)/man1/qutebrowser.1" install -Dm644 misc/qutebrowser.desktop \ diff --git a/misc/qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml similarity index 100% rename from misc/qutebrowser.appdata.xml rename to misc/org.qutebrowser.qutebrowser.appdata.xml From 61d1cd3f5544989c639f72a429402283a49b2b58 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Sun, 27 Jan 2019 12:21:36 -0800 Subject: [PATCH 116/117] Update changelog We may want to consider changing the desktop filename at some point as well to comply with https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html --- doc/changelog.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a116b53e6..338625936 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -55,6 +55,8 @@ Changed - The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those versions should work without any issues. - The JavaScript `console` object is now available in PAC files. +- The metainfo file `qutebrowser.appdata.xml` is now renamed to + `org.qutebrowser.qutebrowser.appdata.xml`. Fixed ~~~~~ From 8ad7184ca1d67a06b19edef43638581e330badb1 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Sun, 27 Jan 2019 12:44:27 -0800 Subject: [PATCH 117/117] Update changelog --- doc/changelog.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 338625936..6b2bc1d71 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -57,6 +57,8 @@ Changed - The JavaScript `console` object is now available in PAC files. - The metainfo file `qutebrowser.appdata.xml` is now renamed to `org.qutebrowser.qutebrowser.appdata.xml`. +- The `qute-pass` userscript now understands domains in gpg filenames + in addition to directory names. Fixed ~~~~~ @@ -71,7 +73,7 @@ Fixed `content.cookies.accept = no-3rdparty` from working properly on some pages like GMail. However, the default for `content.cookies.accept` is still `all` to be in line with what other browsers do. -- `:navigate` not incrementing in anchors or queries or anchors. +- `:navigate` not incrementing in anchors or queries. - Crash when trying to use a proxy requiring authentication with QtWebKit. - Slashes in search terms are now percent-escaped. - When `scrolling.bar = True` was set in versions before v1.5.0, this now