diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..f623e0a93 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +* @The-Compiler + +qutebrowser/browser/history.py @The-Compiler @rcorre +qutebrowser/completion/* @The-Compiler @rcorre +qutebrowser/misc/sql.py @The-Compiler @rcorre +tests/end2end/features/completion.feature @The-Compiler @rcorre +tests/end2end/features/test_completion_bdd.py @The-Compiler @rcorre +tests/unit/browser/test_history.py @The-Compiler @rcorre +tests/unit/completion/* @The-Compiler @rcorre +tests/unit/misc/test_sql.py @The-Compiler @rcorre diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 14fd41b88..8a41c625e 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -14,6 +14,42 @@ This project adheres to http://semver.org/[Semantic Versioning]. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +v0.1.0 (unreleased) +------------------- + +Breaking changes +~~~~~~~~~~~~~~~~ + +- Support for legacy QtWebKit (before 5.212 which is distributed + independently from Qt) is dropped. +- Support for Python 3.4 is dropped. +- Support for Qt before 5.7 is dropped. +- New dependency on the QtSql module and Qt sqlite support. +- New dependency on ruamel.yaml; dropped PyYAML dependency. +- The QtWebEngine backend is now used by default if available. +- New config system which ignores the old config file. + +Major changes +~~~~~~~~~~~~~ + +- New completion engine based on sqlite, which allows to complete + the entire browsing history. +- Completely rewritten configuration system. + +Fixes +~~~~~ + +- Exiting fullscreen via `:fullscreen` or buttons on a page now + restores the correct previous window state (maximized/fullscreen). + +v0.11.1 (unreleased) +-------------------- + +Fixes +~~~~~ + +- Fixed empty space being shown after tabs in the tabbar in some cases. + v0.11.0 ------- diff --git a/README.asciidoc b/README.asciidoc index 1c7e42c33..3ed3bc054 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -163,9 +163,9 @@ Contributors, sorted by the number of commits in descending order: * Kevin Velghe * Raphael Pierzina * Joel Torstensson +* Jay Kamat * Patric Schmitz * Tarcisio Fedrizzi -* Jay Kamat * Claude * Philipp Hansch * Fritz Reichwald @@ -193,6 +193,7 @@ Contributors, sorted by the number of commits in descending order: * knaggita * Oliver Caldwell * Nikolay Amiantov +* Marius * Julian Weigt * Tomasz Kramkowski * Sebastian Frysztak @@ -210,6 +211,7 @@ Contributors, sorted by the number of commits in descending order: * Halfwit * David Vogt * Claire Cavanaugh +* Christian Helbling * rikn00 * kanikaa1234 * haitaka @@ -217,7 +219,6 @@ Contributors, sorted by the number of commits in descending order: * Michał Góral * Michael Ilsaas * Martin Zimmermann -* Marius * Link * Jussi Timperi * Cosmin Popescu diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 97167758f..9531af7aa 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -38,7 +38,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, typing) + objreg, utils, typing, debug) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess from qutebrowser.completion.models import urlmodel, miscmodels @@ -2166,6 +2166,10 @@ class CommandDispatcher: window = self._tabbed_browser.window() if window.isFullScreen(): - window.showNormal() + window.setWindowState( + window.state_before_fullscreen & ~Qt.WindowFullScreen) else: + window.state_before_fullscreen = window.windowState() window.showFullScreen() + log.misc.debug('state before fullscreen: {}'.format( + debug.qflags_key(Qt, window.state_before_fullscreen))) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 0a4c2dfc7..ab4500364 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -615,6 +615,7 @@ class WebEngineTab(browsertab.AbstractTab): def shutdown(self): self.shutting_down.emit() + self.action.exit_fullscreen() if qtutils.version_check('5.8', exact=True): # WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58563 @@ -715,7 +716,8 @@ class WebEngineTab(browsertab.AbstractTab): @pyqtSlot() def _on_load_started(self): """Clear search when a new load is started if needed.""" - if qtutils.version_check('5.9'): + if (qtutils.version_check('5.9') and + not qtutils.version_check('5.9.2')): # WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-61506 self.search.clear() diff --git a/qutebrowser/html/backend-warning.html b/qutebrowser/html/backend-warning.html index ffff0e59b..2b631d6a5 100644 --- a/qutebrowser/html/backend-warning.html +++ b/qutebrowser/html/backend-warning.html @@ -70,6 +70,8 @@ the qute://settings page or caret browsing). {{ install_webengine('qt5-qtwebengine') }} {% elif distribution.parsed == Distribution.opensuse %} {{ install_webengine('libqt5-qtwebengine') }} +{% elif distribution.parsed == Distribution.gentoo %} + {{ install_webengine('dev-qt/qtwebengine') }} {% else %} {{ unknown_system() }} {% endif %} diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 067b536bf..2f43ba58e 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config -from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils +from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, + debug) from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget, completer @@ -123,6 +124,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. @@ -217,6 +219,8 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) + 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: @@ -483,9 +487,12 @@ class MainWindow(QWidget): @pyqtSlot(bool) def _on_fullscreen_requested(self, on): if on: + self.state_before_fullscreen = self.windowState() self.showFullScreen() - else: - self.showNormal() + elif self.isFullScreen(): + 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() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index c7823f84e..eeee223aa 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -761,6 +761,17 @@ class TabBarStyle(QCommonStyle): rct = super().subElementRect(sr, opt, widget) return rct else: + try: + # We need this so the left scroll button is aligned properly. + # Otherwise, empty space will be shown after the last tab even + # though the button width is set to 0 + # + # QStyle.SE_TabBarScrollLeftButton was added in Qt 5.7 + if sr == QStyle.SE_TabBarScrollLeftButton: + return super().subElementRect(sr, opt, widget) + except AttributeError: + pass + return self._style.subElementRect(sr, opt, widget) def _tab_layout(self, opt): diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 86af58d3e..fe3e1aedb 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -326,12 +326,11 @@ def version(): lines += _module_versions() - lines += ['pdf.js: {}'.format(_pdfjs_version())] - lines += ['sqlite: {}'.format(sql.version())] - lines += [ - 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), - '', + 'pdf.js: {}'.format(_pdfjs_version()), + 'sqlite: {}'.format(sql.version()), + 'QtNetwork SSL: {}\n'.format(QSslSocket.sslLibraryVersionString() + if QSslSocket.supportsSsl() else 'no'), ] qapp = QApplication.instance() diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index e6a4f89e1..bb5733cad 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -292,6 +292,14 @@ def build_sdist(): return artifacts +def read_github_token(): + """Read the GitHub API token from disk.""" + token_file = os.path.join(os.path.expanduser('~'), '.gh_token') + with open(token_file, encoding='ascii') as f: + token = f.read().strip() + return token + + def github_upload(artifacts, tag): """Upload the given artifacts to GitHub. @@ -302,9 +310,7 @@ def github_upload(artifacts, tag): import github3 utils.print_title("Uploading to github...") - token_file = os.path.join(os.path.expanduser('~'), '.gh_token') - with open(token_file, encoding='ascii') as f: - token = f.read().strip() + token = read_github_token() gh = github3.login(token=token) repo = gh.repository('qutebrowser', 'qutebrowser') @@ -341,6 +347,12 @@ def main(): upload_to_pypi = False + if args.upload is not None: + # Fail early when trying to upload without github3 installed + # or without API token + import github3 # pylint: disable=unused-variable + read_github_token() + if os.name == 'nt': if sys.maxsize > 2**32: # WORKAROUND diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 1227f4d3e..a912d0fd4 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -21,6 +21,7 @@ import io import sys +import collections import os.path import subprocess import contextlib @@ -475,29 +476,32 @@ class ImportFake: """A fake for __import__ which is used by the import_fake fixture. Attributes: - exists: A dict mapping module names to bools. If True, the import will - success. Otherwise, it'll fail with ImportError. + modules: A dict mapping module names to bools. If True, the import will + success. Otherwise, it'll fail with ImportError. version_attribute: The name to use in the fake modules for the version attribute. version: The version to use for the modules. _real_import: Saving the real __import__ builtin so the imports can be - done normally for modules not in self.exists. + done normally for modules not in self. modules. """ def __init__(self): - self.exists = { - 'sip': True, - 'colorama': True, - 'pypeg2': True, - 'jinja2': True, - 'pygments': True, - 'yaml': True, - 'cssutils': True, - 'typing': True, - 'PyQt5.QtWebEngineWidgets': True, - 'PyQt5.QtWebKitWidgets': True, - 'OpenGL': True, - } + self.modules = collections.OrderedDict([ + ('sip', True), + ('colorama', True), + ('pypeg2', True), + ('jinja2', True), + ('pygments', True), + ('yaml', True), + ('cssutils', True), + ('typing', True), + ('OpenGL', True), + ('PyQt5.QtWebEngineWidgets', True), + ('PyQt5.QtWebKitWidgets', True), + ]) + self.no_version_attribute = ['sip', 'typing', + 'PyQt5.QtWebEngineWidgets', + 'PyQt5.QtWebKitWidgets'] self.version_attribute = '__version__' self.version = '1.2.3' self._real_import = builtins.__import__ @@ -509,10 +513,10 @@ class ImportFake: The imported fake module, or None if normal importing should be used. """ - if name not in self.exists: + if name not in self.modules: # Not one of the modules to test -> use real import return None - elif self.exists[name]: + elif self.modules[name]: ns = types.SimpleNamespace() if self.version_attribute is not None: setattr(ns, self.version_attribute, self.version) @@ -551,14 +555,14 @@ class TestModuleVersions: """Tests for _module_versions().""" - @pytest.mark.usefixtures('import_fake') - def test_all_present(self): + def test_all_present(self, import_fake): """Test with all modules present in version 1.2.3.""" - expected = ['sip: yes', 'colorama: 1.2.3', 'pypeg2: 1.2.3', - 'jinja2: 1.2.3', 'pygments: 1.2.3', 'yaml: 1.2.3', - 'cssutils: 1.2.3', 'typing: yes', 'OpenGL: 1.2.3', - 'PyQt5.QtWebEngineWidgets: yes', - 'PyQt5.QtWebKitWidgets: yes'] + expected = [] + for name in import_fake.modules: + if name in import_fake.no_version_attribute: + expected.append('{}: yes'.format(name)) + else: + expected.append('{}: 1.2.3'.format(name)) assert version._module_versions() == expected @pytest.mark.parametrize('module, idx, expected', [ @@ -574,36 +578,31 @@ class TestModuleVersions: idx: The index where the given text is expected. expected: The expected text. """ - import_fake.exists[module] = False + import_fake.modules[module] = False assert version._module_versions()[idx] == expected - @pytest.mark.parametrize('value, expected', [ - ('VERSION', ['sip: yes', 'colorama: 1.2.3', 'pypeg2: yes', - 'jinja2: yes', 'pygments: yes', 'yaml: yes', - 'cssutils: yes', 'typing: yes', 'OpenGL: yes', - 'PyQt5.QtWebEngineWidgets: yes', - 'PyQt5.QtWebKitWidgets: yes']), - ('SIP_VERSION_STR', ['sip: 1.2.3', 'colorama: yes', 'pypeg2: yes', - 'jinja2: yes', 'pygments: yes', 'yaml: yes', - 'cssutils: yes', 'typing: yes', 'OpenGL: yes', - 'PyQt5.QtWebEngineWidgets: yes', - 'PyQt5.QtWebKitWidgets: yes']), - (None, ['sip: yes', 'colorama: yes', 'pypeg2: yes', 'jinja2: yes', - 'pygments: yes', 'yaml: yes', 'cssutils: yes', 'typing: yes', - 'OpenGL: yes', 'PyQt5.QtWebEngineWidgets: yes', - 'PyQt5.QtWebKitWidgets: yes']), + @pytest.mark.parametrize('attribute, expected_modules', [ + ('VERSION', ['colorama']), + ('SIP_VERSION_STR', ['sip']), + (None, []), ]) - def test_version_attribute(self, value, expected, import_fake): + def test_version_attribute(self, attribute, expected_modules, import_fake): """Test with a different version attribute. VERSION is tested for old colorama versions, and None to make sure things still work if some package suddenly doesn't have __version__. Args: - value: The name of the version attribute. + attribute: The name of the version attribute. expected: The expected return value. """ - import_fake.version_attribute = value + import_fake.version_attribute = attribute + expected = [] + for name in import_fake.modules: + if name in expected_modules: + expected.append('{}: 1.2.3'.format(name)) + else: + expected.append('{}: yes'.format(name)) assert version._module_versions() == expected @pytest.mark.parametrize('name, has_version', [ @@ -759,14 +758,16 @@ class FakeQSslSocket: Attributes: _version: What QSslSocket::sslLibraryVersionString() should return. + _support: Whether SSL is supported. """ - def __init__(self, version=None): + def __init__(self, version=None, support=True): self._version = version + self._support = support def supportsSsl(self): """Fake for QSslSocket::supportsSsl().""" - return True + return self._support def sslLibraryVersionString(self): """Fake for QSslSocket::sslLibraryVersionString().""" @@ -799,18 +800,30 @@ def test_chromium_version_unpatched(qapp): assert version._chromium_version() not in ['', 'unknown', 'unavailable'] -@pytest.mark.parametrize(['git_commit', 'frozen', 'style', 'with_webkit', - 'known_distribution'], [ - (True, False, True, True, True), # normal - (False, False, True, True, True), # no git commit - (True, True, True, True, True), # frozen - (True, True, False, True, True), # no style - (True, False, True, False, True), # no webkit - (True, False, True, 'ng', True), # QtWebKit-NG - (True, False, True, True, False), # unknown Linux distribution -]) # pylint: disable=too-many-locals -def test_version_output(git_commit, frozen, style, with_webkit, - known_distribution, stubs, monkeypatch, init_sql): +class VersionParams: + + def __init__(self, name, git_commit=True, frozen=False, style=True, + with_webkit=True, known_distribution=True, ssl_support=True): + self.name = name + self.git_commit = git_commit + self.frozen = frozen + self.style = style + self.with_webkit = with_webkit + self.known_distribution = known_distribution + self.ssl_support = ssl_support + + +@pytest.mark.parametrize('params', [ + VersionParams('normal'), + VersionParams('no-git-commit', git_commit=False), + VersionParams('frozen', frozen=True), + VersionParams('no-style', style=False), + VersionParams('no-webkit', with_webkit=False), + VersionParams('webkit-ng', with_webkit='ng'), + VersionParams('unknown-dist', known_distribution=False), + VersionParams('no-ssl', ssl_support=False), +], ids=lambda param: param.name) +def test_version_output(params, stubs, monkeypatch): """Test version.version().""" class FakeWebEngineProfile: def httpUserAgent(self): @@ -820,37 +833,38 @@ def test_version_output(git_commit, frozen, style, with_webkit, patches = { 'qutebrowser.__file__': os.path.join(import_path, '__init__.py'), 'qutebrowser.__version__': 'VERSION', - '_git_str': lambda: ('GIT COMMIT' if git_commit else None), + '_git_str': lambda: ('GIT COMMIT' if params.git_commit else None), 'platform.python_implementation': lambda: 'PYTHON IMPLEMENTATION', 'platform.python_version': lambda: 'PYTHON VERSION', 'PYQT_VERSION_STR': 'PYQT VERSION', 'earlyinit.qt_version': lambda: 'QT VERSION', '_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'], '_pdfjs_version': lambda: 'PDFJS VERSION', - 'QSslSocket': FakeQSslSocket('SSL VERSION'), + 'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support), 'platform.platform': lambda: 'PLATFORM', 'platform.architecture': lambda: ('ARCHITECTURE', ''), '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'], '_path_info': lambda: {'PATH DESC': 'PATH NAME'}, - 'QApplication': (stubs.FakeQApplication(style='STYLE') if style else + 'QApplication': (stubs.FakeQApplication(style='STYLE') + if params.style else stubs.FakeQApplication(instance=None)), 'QLibraryInfo.location': (lambda _loc: 'QT PATH'), 'sql.version': lambda: 'SQLITE VERSION', } substitutions = { - 'git_commit': '\nGit commit: GIT COMMIT' if git_commit else '', - 'style': '\nStyle: STYLE' if style else '', + 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', + 'style': '\nStyle: STYLE' if params.style else '', 'qt': 'QT VERSION', - 'frozen': str(frozen), + 'frozen': str(params.frozen), 'import_path': import_path, } - if with_webkit: + if params.with_webkit: patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION' patches['objects.backend'] = usertypes.Backend.QtWebKit patches['QWebEngineProfile'] = None - if with_webkit == 'ng': + if params.with_webkit == 'ng': backend = 'QtWebKit-NG' patches['qtutils.is_qtwebkit_ng'] = lambda: True else: @@ -863,7 +877,7 @@ def test_version_output(git_commit, frozen, style, with_webkit, patches['QWebEngineProfile'] = FakeWebEngineProfile substitutions['backend'] = 'QtWebEngine (Chromium CHROMIUMVERSION)' - if known_distribution: + if params.known_distribution: patches['distribution'] = lambda: version.DistributionInfo( parsed=version.Distribution.arch, version=None, pretty='LINUX DISTRIBUTION', id='arch') @@ -875,10 +889,12 @@ def test_version_output(git_commit, frozen, style, with_webkit, substitutions['linuxdist'] = '' substitutions['osinfo'] = 'OS INFO 1\nOS INFO 2\n' + substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no' + for attr, val in patches.items(): monkeypatch.setattr('qutebrowser.utils.version.' + attr, val) - if frozen: + if params.frozen: monkeypatch.setattr(sys, 'frozen', True, raising=False) else: monkeypatch.delattr(sys, 'frozen', raising=False) @@ -895,7 +911,7 @@ def test_version_output(git_commit, frozen, style, with_webkit, MODULE VERSION 2 pdf.js: PDFJS VERSION sqlite: SQLITE VERSION - SSL: SSL VERSION + QtNetwork SSL: {ssl} {style} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen}