From ac1b59a3113e339f16896e32d41a1e5d6178dade Mon Sep 17 00:00:00 2001 From: Christian Helbling Date: Thu, 18 Oct 2018 17:48:49 +0200 Subject: [PATCH 001/258] 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/258] 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/258] 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 2e562a926b659f084014876b7b11a8aa27884489 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 6 Nov 2018 08:05:17 -0500 Subject: [PATCH 004/258] Don't complete url and title from same search word. Resolves #4411: > When opening a webpage, the suggested results will include those whose > URL ends with the beginning of the string you've typed and whose title > begins with the rest of the string. By joining the url and title with a space, we ensure that the last word of the url and the first word of the title are treated as separate words. --- qutebrowser/completion/models/histcategory.py | 2 +- tests/unit/completion/test_histcategory.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py index 7c9fc920d..b6d5f0465 100644 --- a/qutebrowser/completion/models/histcategory.py +++ b/qutebrowser/completion/models/histcategory.py @@ -76,7 +76,7 @@ class HistoryCategory(QSqlQueryModel): # given the search term "a b", the WHERE clause would be: # ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%') where_clause = ' AND '.join( - "(url || title) LIKE :{} escape '\\'".format(i) + "(url || ' ' || title) LIKE :{} escape '\\'".format(i) for i in range(len(words))) # replace ' in timestamp-format to avoid breaking the query diff --git a/tests/unit/completion/test_histcategory.py b/tests/unit/completion/test_histcategory.py index b36125a4a..02a6cfd1e 100644 --- a/tests/unit/completion/test_histcategory.py +++ b/tests/unit/completion/test_histcategory.py @@ -82,6 +82,14 @@ def hist(init_sql, config_stub): ("ample itle", [('example.com', 'title'), ('example.com', 'nope')], [('example.com', 'title')]), + + # https://github.com/qutebrowser/qutebrowser/issues/4411 + ("mlfreq", + [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')], + []), + ("ml freq", + [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')], + [('https://qutebrowser.org/FAQ.html', 'Frequently Asked Questions')]), ]) def test_set_pattern(pattern, before, after, model_validator, hist): """Validate the filtering and sorting results of set_pattern.""" From bf10f483e14bd47a9ebd4dacded0d608d4304642 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 13 Nov 2018 20:44:57 -0500 Subject: [PATCH 005/258] Fix sql comment to match updated code. --- qutebrowser/completion/models/histcategory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py index b6d5f0465..83eafef50 100644 --- a/qutebrowser/completion/models/histcategory.py +++ b/qutebrowser/completion/models/histcategory.py @@ -74,7 +74,8 @@ class HistoryCategory(QSqlQueryModel): # build a where clause to match all of the words in any order # given the search term "a b", the WHERE clause would be: - # ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%') + # ((url || ' ' || title) LIKE '%a%') AND + # ((url || ' ' || title) LIKE '%b%') where_clause = ' AND '.join( "(url || ' ' || title) LIKE :{} escape '\\'".format(i) for i in range(len(words))) From fa8462555668ff4d0ae25becc8a81f0c7357c967 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Nov 2018 19:56:55 +0100 Subject: [PATCH 006/258] travis: Remove "sudo: false" See https://blog.travis-ci.com/2018-10-04-combining-linux-infrastructures --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb11e0c3c..e7804cf4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false dist: trusty language: python group: edge From 02e350779b9708b3c0609125934c35932317b0eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Nov 2018 19:59:19 +0100 Subject: [PATCH 007/258] travis: Upgrade to Xenial See https://blog.travis-ci.com/2018-11-08-xenial-release --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e7804cf4d..5b918a393 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: trusty +dist: xenial language: python group: edge python: 3.6 @@ -26,11 +26,8 @@ matrix: - xfonts-base - os: linux env: TESTENV=py36-pyqt511-cov - # https://github.com/travis-ci/travis-ci/issues/9069 - os: linux python: 3.7 - sudo: required - dist: xenial env: TESTENV=py37-pyqt511 - os: osx env: TESTENV=py37 OSX=sierra From 6f60f10b8e5513cc97b4b91c6b5a01458752760a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Nov 2018 20:03:05 +0100 Subject: [PATCH 008/258] travis: Add Windows --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5b918a393..99fdc3128 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,9 @@ matrix: env: TESTENV=py37 OSX=sierra osx_image: xcode9.2 language: generic + - os: windows + python: 3.7 + env: TESTENV=py37-pyqt511 # https://github.com/qutebrowser/qutebrowser/issues/2013 # - os: osx # env: TESTENV=py35 OSX=yosemite From e1cb00bed332b29157adf97eb0d68c4d907ec0c7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 14 Nov 2018 20:03:15 +0100 Subject: [PATCH 009/258] Recompile requirements --- misc/requirements/requirements-tests.txt | 8 ++++---- misc/requirements/requirements-tox.txt | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index c17f0d6b1..27390c85a 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -5,8 +5,8 @@ attrs==18.2.0 backports.functools-lru-cache==1.5 beautifulsoup4==4.6.3 cheroot==6.5.2 -click==7.0 -# colorama==0.3.9 +Click==7.0 +# colorama==0.4.0 coverage==4.5.2 EasyProcess==0.2.3 fields==5.0.0 @@ -17,14 +17,14 @@ hypothesis==3.82.1 itsdangerous==1.1.0 # Jinja2==2.10 Mako==1.0.7 -# MarkupSafe==1.0 +# MarkupSafe==1.1.0 more-itertools==4.3.0 parse==1.9.0 parse-type==0.4.2 pluggy==0.8.0 py==1.7.0 py-cpuinfo==4.0.0 -pytest==3.10.1 +pytest==4.0.0 pytest-bdd==3.0.0 pytest-benchmark==3.1.1 pytest-cov==2.6.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 9cda12622..097859b6e 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -1,5 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py +filelock==3.0.10 pluggy==0.8.0 py==1.7.0 six==1.11.0 From 19cb3598ccf6f831132bf60ccb086c91b9f5107b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 15 Nov 2018 09:47:27 +0100 Subject: [PATCH 010/258] Revert "travis: Add Windows" This reverts commit 6f60f10b8e5513cc97b4b91c6b5a01458752760a. Was supposed to be in a branch. --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 99fdc3128..5b918a393 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,9 +33,6 @@ matrix: env: TESTENV=py37 OSX=sierra osx_image: xcode9.2 language: generic - - os: windows - python: 3.7 - env: TESTENV=py37-pyqt511 # https://github.com/qutebrowser/qutebrowser/issues/2013 # - os: osx # env: TESTENV=py35 OSX=yosemite From 7ee68c43dfca6adce094c9e90e8a8a780dd5d618 Mon Sep 17 00:00:00 2001 From: Raphael Das Gupta Date: Fri, 16 Nov 2018 00:39:54 +0100 Subject: [PATCH 011/258] Fix grammar: This kind of question requires "does" in English --- doc/faq.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index c4176a438..3d3d4f7bc 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -215,7 +215,7 @@ What's the difference between insert and passthrough mode?:: be useful to rebind escape to something else in passthrough mode only, to be able to send an escape keypress to the website. -Why takes it longer to open an URL in qutebrowser than in chromium?:: +Why does it take longer to open an URL in qutebrowser than in chromium?:: When opening an 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 From bca47ff8797a35ef02bd43a6353c804045d26fd6 Mon Sep 17 00:00:00 2001 From: Raphael Das Gupta Date: Fri, 16 Nov 2018 00:42:17 +0100 Subject: [PATCH 012/258] Fix grammar: add required comma --- doc/faq.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 3d3d4f7bc..cffdce7f5 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -216,7 +216,7 @@ What's the difference between insert and passthrough mode?:: able to send an escape keypress to the website. Why does it take longer to open an URL in qutebrowser than in chromium?:: - When opening an URL in an existing instance the normal qutebrowser + When opening an 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 where the URL is then passed to. This takes some time. From 409e0d33cdd53899685b88edfc8a9b81b0edb2e6 Mon Sep 17 00:00:00 2001 From: Raphael Das Gupta Date: Fri, 16 Nov 2018 00:43:18 +0100 Subject: [PATCH 013/258] fix grammar: "to which" instead of "to where" --- doc/faq.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index cffdce7f5..a844bcfde 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -219,7 +219,7 @@ Why does it take longer to open an URL in qutebrowser than in chromium?:: When opening an 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 - where the URL is then passed to. This takes some time. + 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 From 2152081d82bb9ea43bfd5562c6f964ff4026f193 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 16 Nov 2018 12:36:58 +0100 Subject: [PATCH 014/258] s/an URL/a URL/g --- doc/changelog.asciidoc | 4 ++-- doc/faq.asciidoc | 4 ++-- qutebrowser/app.py | 4 ++-- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/config/config.py | 2 +- qutebrowser/javascript/greasemonkey_wrapper.js | 2 +- scripts/dev/misc_checks.py | 3 ++- tests/unit/browser/webkit/network/test_pac.py | 2 +- tests/unit/config/test_config.py | 4 ++-- tests/unit/utils/test_urlmatch.py | 2 +- 11 files changed, 16 insertions(+), 15 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 22c5fd333..41515d287 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -1244,7 +1244,7 @@ Added - New `:debug-log-filter` command to change console log filtering on-the-fly. - New `:debug-log-level` command to change the console loglevel on-the-fly. - New `general -> yank-ignored-url-parameters` option to configure which URL - parameters (like `utm_source` etc.) to strip off when yanking an URL. + parameters (like `utm_source` etc.) to strip off when yanking a URL. - Support for the https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API[HTML5 page visibility API] - New `readability` userscript which shows a readable version of a page (using @@ -1355,7 +1355,7 @@ Changed - `:hint` has a new `--add-history` argument to add the URL to the history for yank/spawn targets. - `:set` now cycles through values if more than one argument is given. -- `:open` now opens `default-page` without an URL even without `-t`/`-b`/`-w` given. +- `:open` now opens `default-page` without a URL even without `-t`/`-b`/`-w` given. Deprecated ~~~~~~~~~~ diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index a844bcfde..6687917c7 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -215,8 +215,8 @@ What's the difference between insert and passthrough mode?:: 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 an URL in qutebrowser than in chromium?:: - When opening an URL in an existing instance, the normal qutebrowser +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. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 1cfcc8496..7d1b61131 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -303,10 +303,10 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None): def open_url(url, target=None, no_raise=False, via_ipc=True): - """Open an URL in new window/tab. + """Open a URL in new window/tab. Args: - url: An URL to open. + url: A URL to open. target: same as new_instance_open_target (used as a default). no_raise: suppress target window raising. via_ipc: Whether the arguments were transmitted over IPC. diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index bb0b31626..b2be458ce 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -845,7 +845,7 @@ class AbstractTab(QWidget): @pyqtSlot(QUrl) def _on_predicted_navigation(self, url): - """Adjust the title if we are going to visit an URL soon.""" + """Adjust the title if we are going to visit a URL soon.""" qtutils.ensure_valid(url) url_string = url.toDisplayString() log.webview.debug("Predicted navigation: {}".format(url_string)) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index eba9174b1..0f1d368e9 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1390,7 +1390,7 @@ class WebEngineTab(browsertab.AbstractTab): @pyqtSlot(QUrl) def _on_predicted_navigation(self, url): - """If we know we're going to visit an URL soon, change the settings. + """If we know we're going to visit a URL soon, change the settings. This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 """ diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index facfcc553..1f2e45741 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -357,7 +357,7 @@ class Config(QObject): """Get an object which can be mutated, e.g. in a config.py. If a pattern is given, return the value for that pattern. - Note that it's impossible to get a mutable object for an URL as we + Note that it's impossible to get a mutable object for a URL as we wouldn't know what pattern to apply. """ self.get_opt(name) # To make sure it exists diff --git a/qutebrowser/javascript/greasemonkey_wrapper.js b/qutebrowser/javascript/greasemonkey_wrapper.js index 457118696..f8c36151a 100644 --- a/qutebrowser/javascript/greasemonkey_wrapper.js +++ b/qutebrowser/javascript/greasemonkey_wrapper.js @@ -60,7 +60,7 @@ details.method = details.method ? details.method.toUpperCase() : "GET"; if (!details.url) { - throw new Error("GM_xmlhttpRequest requires an URL."); + throw new Error("GM_xmlhttpRequest requires a URL."); } // build XMLHttpRequest object diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 299246448..27ff0105b 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -89,7 +89,8 @@ def check_spelling(): '[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted', '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily', '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting', - 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations'} + 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations', + '[Aa]n [Uu][Rr][Ll]'} # Words which look better when splitted, but might need some fine tuning. words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence', diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py index ff0346411..8c03c6cee 100644 --- a/tests/unit/browser/webkit/network/test_pac.py +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -183,7 +183,7 @@ def test_fail_return(): ]) @pytest.mark.parametrize('from_file', [True, False]) def test_secret_url(url, has_secret, from_file): - """Make sure secret parts in an URL are stripped correctly. + """Make sure secret parts in a URL are stripped correctly. The following parts are considered secret: - If the PAC info is loaded from a local file, nothing. diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 672faf04a..946770bf1 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -473,7 +473,7 @@ class TestConfig: assert conf.get('colors.completion.category.fg') == QColor('white') def test_get_for_url(self, conf): - """Test conf.get() with an URL/pattern.""" + """Test conf.get() with a URL/pattern.""" pattern = urlmatch.UrlPattern('*://example.com/') name = 'content.javascript.enabled' conf.set_obj(name, False, pattern=pattern) @@ -484,7 +484,7 @@ class TestConfig: (False, configutils.UNSET) ]) def test_get_for_url_fallback(self, conf, fallback, expected): - """Test conf.get() with an URL and fallback.""" + """Test conf.get() with a URL and fallback.""" value = conf.get('content.javascript.enabled', url=QUrl('https://example.com/'), fallback=fallback) diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py index e569c51b8..1dd57a5e9 100644 --- a/tests/unit/utils/test_urlmatch.py +++ b/tests/unit/utils/test_urlmatch.py @@ -142,7 +142,7 @@ def test_parse_path(pattern, path): ("data:monkey", 'data', None, 'monkey'), # existing scheme ]) def test_lightweight_patterns(pattern, scheme, host, path): - """Make sure we can leave off parts of an URL. + """Make sure we can leave off parts of a URL. This is a deviation from Chromium to make patterns more user-friendly. """ From a641dde9de657c8c58344fb8052eddb764c58f52 Mon Sep 17 00:00:00 2001 From: Winny Date: Sun, 18 Nov 2018 01:50:03 -0600 Subject: [PATCH 015/258] Change phrases to sound more definite/concrete --- doc/help/configuring.asciidoc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 57e5528c0..280a9921a 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -19,10 +19,10 @@ hand, you can simply use those - see <> for details. For more advanced configuration, you can write a `config.py` file - see -<>. As soon as a `config.py` +<>. When `config.py` exists, the `autoconfig.yml` file **is not read anymore** by default. You need -to <> if you want settings done via -`:set`/`:bind` to still persist. +to <> if you want settings changed via +`:set`/`:bind` to persist between restarts. [[autoconfig]] Configuring qutebrowser via the user interface @@ -229,18 +229,18 @@ Loading `autoconfig.yml` ~~~~~~~~~~~~~~~~~~~~~~~~ All customization done via the UI (`:set`, `:bind` and `:unbind`) is -stored in the `autoconfig.yml` file, which is not loaded automatically as soon -as a `config.py` exists. If you want those settings to be loaded, you'll need to -explicitly load the `autoconfig.yml` file in your `config.py` by doing: +stored in file `autoconfig.yml`. When file `config.py` exists, `autoconfig.yml` +is not loaded automatically. To load `autoconfig.yml` automatically, add the +following snippet to `config.py`: -.config.py: [source,python] ---- config.load_autoconfig() ---- -If you do so at the top of your file, your `config.py` settings will take -precedence as they overwrite the settings done in `autoconfig.yml`. +You can configure which file overrides the other by the location of the above code snippet. +Place the snippet at the top to allow `config.py` override `autoconfig.yml` settings. +Place the snippet at the bottom to for the opposite effect. Importing other modules ~~~~~~~~~~~~~~~~~~~~~~~ From de166f71dcd9f2b4b3c72d4203eb755f548419b4 Mon Sep 17 00:00:00 2001 From: Winny Date: Sun, 18 Nov 2018 01:53:45 -0600 Subject: [PATCH 016/258] Fixup --- doc/help/configuring.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 280a9921a..9e6f2a3c7 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -239,8 +239,8 @@ config.load_autoconfig() ---- You can configure which file overrides the other by the location of the above code snippet. -Place the snippet at the top to allow `config.py` override `autoconfig.yml` settings. -Place the snippet at the bottom to for the opposite effect. +Place the snippet at the top to allow `config.py` to override `autoconfig.yml`. +Place the snippet at the bottom for the opposite effect. Importing other modules ~~~~~~~~~~~~~~~~~~~~~~~ From a2ba0b4c7661969ae1184fdcda12cda21340c6b1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 08:38:54 +0100 Subject: [PATCH 017/258] Thanks to the HSR! --- .github/img/hsr.png | Bin 0 -> 12922 bytes README.asciidoc | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 .github/img/hsr.png diff --git a/.github/img/hsr.png b/.github/img/hsr.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1312f6d3539983e63ef42e46f3db4386fc46be GIT binary patch literal 12922 zcmaKzWl$Vl*ro>w5Ind9cXti$Ft`K{?i$=RKya7AU4uJ=6WpEP?(WXcyZdA7tJ>PG z?yl3*Q>VM0WA}YMGvUgL(kO@ohyVZpMOH>a6##(x3Ms21Kts-->t9I#fG>Am>RK+U zM((7JP7dZ)wq~R*o{nauW*%1N0D#AG5y&dxD_@M%#|@S zb2AXc)IR_B2zsQ`ZlUXBc=BE=@KA}yTZ!EWr+pdh=P|#^Ng(Km+H3y0Hks|;MLZM3 z^?q0J(XlJ+|5C7P_FC@oXftf{w4U%m>-qNd9J|hBG9qvh&C5|m}y*G_qr%piHjHgLFC#nJ&0%i$4cPeW5dtj ze@P$<5dVp-(LG-=)3QZ!QAJhLGr?%nX**PgzWa)AtQ`%J&QVf9Nw(WF{vdOu+YOUu zUuE-}-(X{o#!Jeb!^Vfy{7kd;HT@n>Q;x=twT&=w{8z;}8>#+6O0(yT)PPW&YVFf9 ztg8{v@m!2zu@KVv$ofRhD8!UR35RFxZ|lfE`0E}G7MU8J>+P>M)R{ew-lIp}I_y8x z8|`8juPe|EFB1qjW(NhSvp&de#*21#eoPu;Z~iKRnP$Tb*7O)Gvm&o*v>Lv@0V9X}GtWp4{8}FikG6 zYuc>HDw*yam}lcM9j;1i7cBfE%r_-sdi^cH{)P7EN?FLSIy7@mda@kZ20ODji$WI! z>kScB1$QAS&A9OMGmgp z;t;BaXT|i#ls=NxrbnH_i9EAy(WP(A**i}EHcRZ11nwGk)Un|(Gt?bX?Z}Wvr`wY} z$!s`1>ex*EG6N~&9H-haEgqhX*?qSs|JT7Y&*aQySzI$}=EFQ|px4kL_vvPc*SRj; zV-ORwNLKfJgtCB@iEFngYRWtp_05uE-^-lXS0l^wA<@pF>!A=NOSre9E>@3k<@=fX z$R znfL%{lU$X9?XAywzAJSdNbCX%t{xe_lYJU zwV>*c?-XD7Qyo5`99%qmqMTLYRGs3z9497nWU;qQu$^jGexE}`=G_0unjr6)NKd!Yh5^1|T5-N%>?k@I)(%8VDw-hfJ3@!TA8!Qp z_$4=49sy>gvs=Y+RSM=rptF`o89;V=Y*swa^Rsy+t_^@dN`_L3h?ty-!^S%I1UNl{ zPWp-ZyJ(~B?0enzBw$2Uul_<(AmC|_;FfiXZcWO9qK<)+W~zH%`wJ^Ua@c6C=Qk68 z4#|`sl*nqKn=D>i^nPYq1DP2H>j+7w;Wtvk)q_68K>@2JGPI?y!f>BqGAcjMRudr# zHp~s^D9bv09gqmdrspON8Q#?|L@jLDl5ung2f)_PSqj!EeFB(ix%HECO*`jm68Rig zjRe=-H3me3k3hU?Uo4HZiwIZ5?pUzy8fXDrlGPd2$N3Rf#d{*Wypxmx-n^fSRS-!5 z5*4~*iH9jbT74F1GqnhpO}D@OI$6YPz|YdkCbkxAGJ)n6L-i({`~;s}t$UK58b|&mwMv6$XDYA+^ioG4G#Q^7h?4S z#Aq13Ed34l_$})Kxy8-aT-qOV*UT*N%_NGyu$<)yJhiTnoMJX)7AQ;W0q0`gzpKg( zIEyq)b%NfDd=rFOYui>k4bkVKTyy=XDjWqXPf@( zQ4}{)_sA{LYgh$C{7KP2e?CYW9T+}aMHKzWMY;OpnkmsPxNsBMA=RDXJ5?G+=Q+$z zi6r1gUVR!rUjBuSmf4y`OGtmcsB$a2wduVuevzP4R`^Vg0~ z>=u`K30Z9;IsXJ~1U0wsF1xC;>}=X>)8V1SAlj`@Xyg<+$nlt$g0&p;`naCz=#xok zw;TbHHUYA;(PmmXpHMoMg&Q*{V3zZiJuWWdubOuo>gQ2LM{(Wq}m61RJPARtwSC z`X`7Vd3AZ_h5d>6`xLOI`AB-nYTGiHwOr=!WdQh;p#SeDR%pUHx;MYd5{nw*zjMRgR-Y~M9hpiFbdp{zx z8^IX?;p2--#x5odG;~p6=8?&F?XGrN>cKs+$U(PWRseXqtaxGv{%d_CG(~Y4?8A}Sv;_#alE!R`ISc8b|gOVze zym6jxOnWqgpebcAYPw67=WjgRC43dUEdrj7$LKlS0VM=L5?tW+sVrk_YgWArdW+fZ zDIBhx5^+md@ciIR94;?5lq5Jso10NpnZ=a}MwygV2E|Ad5aC^?>w5f&viI>oyNZ78 z4FP4=kiXr)c{7VAs)Qb~aS$N@Y7S3O7Un6BEL0SaRASsvlsXB|@O~?wc-PbeR*jW<`M&D{SsD*}c3xfG+aMt*W} zkC8LLI<0#;*SrvlP+>kX=L^;pW-3xY(P;wBb=%j)nJ*(o`4LKDO4!1fJrbt3Xhk~N z1>mvrNRehbJf&Y>oKOnIg~`DB#{)7<(|%B4=`!tN=D%-(&H;PMW{y@C0iq_#51jd? zO|>|KpYj(O%tR6+u#*spFc7?X8@}NaeJ8udwfCH&pcQEs4n;^kIUqCj-||AR_gKkK z4YXiaHHsJF?$5^=sd7|4ggbl~cHcI~f}0mrfZ})0C)LAt#DtrOdc&-U(3uM4VChEK zj7A@>s)ldM`qLX=YR3jtoMjqt71H~ zA`_T~WIFED6aOG#s|!t_I~_HJU+5(K&2Nu=x)XrxQfgb;V?GzvP)*y-be4c8k`lOu zM5}?n)Uj6oqxl4vr65R{2H7lWgvpx-Um=*!- z-`YJ3T%#tj3$#(c6cr*Eo1eGA1NwZVf{5Fbb}DZD2>Lnb|)r|d6QyP*VMB01D0qj+S$;8Ll0s0MCWV2ETD+MnGh)})}Z4r&1ox^x7+ zViF5BB%dljfxgMcZgi{bILAtkGEa@UyDRnQFb3Adp8RJTl$g!GaF?J@ee;tcox0))WMTj8f7Dfz zuznU)LpgC13|uMtLmUPJM7oT<%*+u)@ml&gR4Z>mD{x1Lk-?MtB6{%YM8wX?C~$rB zrKNlx-V{9T{U}Y*AvbzHhON(eXD>NX%dUBg z9aa}`P`-|X`s3Zt7Bn}+Mq9|dWcMwj*M5jrcyXLq;tB>${*!q}2?4hvoFLrN_oqhz zcEa(uA2)qBzu7xk6@5!iWU$TWiUfD$YvhIP_DT%$9zEDyk?qBLmeGf#RFiiHlej2= zmzCXRZXRY==X%pvX2VeweV6{cFPJb#&K%~k_~3BcU{VKXk7_8%pv!3783RA$6~Yt*B$3p34h-qAf|&pcekZ(Y&H3Pu->fE0GnC(=F`(O^R*Fp~yPs`lXSFDY)Is2a^SamcaUg7V%8+psgqkVOF|r zCf^SdPwL77Ep!OPkx88C{9& zk)z|hWv-*~TUsr?`aL$b$B!0&`!X^7k)SFYoEsPpom5sYJ~O)FcB-89x2#&%}wkaQ!04!jz&*Iwh1@T(*>@AJZHESQFBr8 z%RNIh0?jlBXPiY1%s)fwm_O@s+a3j_X&4sRWA`gsM8jxM3VcKmKPr>&f0Yn&`wvMu z%4jN{Fg^ET8GS>tV{ie(am8m6_tO;B~%8W3+$o|ytmBNs{ConmS`3dPeI`+z%HXZ$O9w%K8Dbx`a zx+*D|nZG~dA2x~Z+>58qt|z}opQW)00S@qsk9)SK+de+5q{^iMYM@B>!HtI=ao8vn zfcM)2JUaRZv3e3SU5ZR0gs33`paaCr=@n_v(Xq*EQyI6fJoK2uM)$aRcp@e2>XP$m zBS?Q=FH+$Ei&Rh3B*e7oiCW2&~I1R+g6m00II6ICc7Dxi>AO zT3eFUzr^ZLSHfcDzrw1`^(Hm)H?QIsB1@2;3dt)gE9)3BA|**CBd?!)n3$M~s!w7I zq{-3F`jUjMtoX;5w3x*?G6cuwzd#`XNIfQcFWk|=cAdsG4p7NowLx_vAW{QWnuXL+7GBLkxT4Ewcph!T(OP6dp3u2l2%vAUGRm)dJ zXJ_Yn3GyRPZQ|HPLRF)zzL(DLKXF%mmGDUXJuUv~@Yx$mQ-yfyy9;G~Td^2^$f4R0 zMiv=bl_VtRE-Nb&T?;9E=FFuX8uu;IgVcc(@bki2`U|XRkMy5qVp5ue`HA0+k4iMS zXk97=^YimS(AaL+gEjq#i;GK}8J8lHc1-_(gf=@~p7Xxg|E>!?TtY!XK`5tMLK`(~ zc66ByMaqk8YvAsrrIhQ-O5Z+9H@LWRrOR7a@Aaw*P7(@QnbY)~e5}US#-^gYT)_YS zw1|U-wR$Gr-~m3&N1Qq_@7XWw(W}xKQBt}!Xng#1a#q)`J?FRy2JEyYAEt|q0^$QM zEKy*Db2l~Vv*|3n!v@Gx>*}!g@<=J)BBR5KqW@Z4otvA3b;V6MPRxTeSHo4Y9`52z zpxRz{!{6nV#`s_eO&@sZ5XN=lNx`#VgDIzIa~-qT0E)cfm*Y^Ly{Ra8wY#v@#n* zB0)c=FH^4M0E&nR4V3}{BQeZc_3SSQ14v~KZ$BdnQ@OwRdP+H7&bLI~&10uk;t^tE z6JqXhT3)Vt-uU`RTUuMY*|-%8DgNtlh6j+IJ*n+y_`IHO&u=ACD}_cN$riO&i}9)6 z&1jO1f@0%KY%t@cIAU7ynyHLUzU$CurzO7ohz|(we_z>K5}paK1Xlk`TziSihsE@iG2!MAWUU(VffqI zd#yg!I!6<$loK`K@s5Z4a(hVcjRaEs2 z*og>u^vRwn?5!2@5kIh!J1s6gNoBd+Wk`WEMb3xJ#uq;aM5Jm1C0;^TEcaB0jfSg* zte1$+?_>Y~EEMDw{-AA;#>@QZ@WSr2{!E_?05pYE6APG@hKGMvU#-UfRh^+eGRb|s zS5r<1zgf}({~0zYtK+SBT$lL=INC*irNT>7_4>ZjSXgqfaW*PiL0(}H4)%228(lan zGYPg4Cqd3LPKMIe)zwUPzQPmxY&J>AkHDwxz;{z5)q%&pPnH240qdunDDL6TP9%vm z@sq+&Q3RRt>=*hZhNUt4bF+Dw*erZO_;b|{OKr(vIQ8Q{pGIp;zT25SkK!u-BrEgU zsmP_(R3=ZRJ>WYv!kyzP7#SIn0|NIjA|oRezdoUS$Z{0lwac0ez-?AWbhvz>XLeD) z4OJgOtAIq#N$nSUp)>;#c=Lz-LkQSN(&1Mpv_C&HZ(wFdxHlk$E~HUEqWJHs0x>!t`qSF8^QTQLnB#iI!~EaM`u|_ zN2deiHZ`7B4xWe@-6IDGD0yBA>_MITckBE6o@FkEI6M~K#|`gJ)jVxDtOdw=I9{x8 z7XkC0#Z>8F*g=1ai>ZY8mH1pVHSTV`y&_Cy&6?!(t z=H}F8$kb8)WiwD;P_epW;9MJzY{%NOx;wDd?7#$UbHDXl%FBu#A?#@*~ z!4@m|`0#MXk`oCy%51&?ESfnkFK4FI{I0$^==J=ZWA^^-uck!#2Oa^z@h9lOl$55F zl;Q|P6Ofc5gz@5 z2X1Q{X5!qvdBQ(Ba&>Klh3=Ujk5+6308Guz)6&{gL0MULhK2!ys*~fxbc~FY=y9S| zRX8{)Ev3)T&ws{{QNti}3;)=xnt=fcATBBCaElGkz#o*5aJtR@TPzO%pdT6iXWNum z&+()EPs3cL$(o;>Q(XoayxKc19jkg7x4gf9>-YBT+DRrW8~%9J9mwqE<&~9jwJn(* z5<+#~m_E!%$HXKF$S%t_T;xe_G_E}{zVF>GjR67qjXR&sH4EF8x2 zf4R5&VP7RfIWs?hcYRG>QGV^wp33s$F$OPi1}O%Yp?%i=NvSd zQ=`LrOjG1DIE5G(X2(|bl$5&a>M(#GC--M)rt9FyYGt3-!|8QFVMz1S&&N%AlT=*} zCk|r5NO5!4FfuYBNQjC1deatRBZ+ykJLkIDh8x!ZYnh;;=`!erbTeP7clLMZ*=GmO zENZ2L|sioX+;Hgcs~Gu8deVhpL&e3qx!9+>h~(lC>anrG5yXwIx;db((7CmH~e%^ zsk;PIUR`a_YReWcGI!pO#mZa!44agk{NCWalJk&Qt63K|xG5a-?zi>&w8!ur8UX2H z%Nh*iP4*9WMAI;sd2b)}7D~Yu69ngbRNi&@`Nsd+|23bNXzA$C(}wv>L=teA|Dg(= zcFpeoDH<9YinvmZ4g=q+n9cqPAVP}n6()!;vzp(K2(db)>niMO$(Kco4 z`-h60({p44CXJFw+xh@d>VQC+R!duz4nS)*Hc{&?_Gy1KgL7+hT31k;u%cXy~UBx?e4iuG$9382-`BwIONR1!Ga+zFROJoAEun44O4C}Ov{>D#!zJJV zuYQapt^WR!PE(*+8q9EdKtPKP&k8)BzUqB9+RDR64H494cYLW)c6JW;2)yA;3)>Pc z?fbLcpD_70Z=1IPgpeo6-i3hpGc*}S9QPlw9M0blU-|0c(9X?)4dPv=^$o`-klEM! zEwE5EpnygZ&B?Vt290%N}|)`w-Hj3zyhV zV>9vf@rjit{^oL-D(L+H1$e$a>NDr@l|QOleZ-8t+CAZW?3foN{cXB^Bu9mToot`O z;zf3@v$#(J9cR;gJ_PcIg;t@TrIDdDhQ5Bc!{~f_sj2aFzgYKt-jx+G+@x9a!3qF} z$L@}Gk*&YGHZ~$lo>5km(UiOg6D=;xMd9t!rv^$DjHhF|JxpX(d0$_+eZ<{fZcT5X zRqMUkp`EX!vVHKjFQkW>Q@t-`W>-1kB1n#CsLW2iQVs2&o_=-T$~@I6D^XGlK4AXK z**!4utRU+5Jen#vPlR7uR`v-l2J8*?g(1dG6)hU#gq z-6D0kzrs+fC#AaMg$4cn=1(vkWm=6YiY_et5tVA?5E<$1Ln!1GD2hovU=V}p+8AuE zi9jGscDxj?YA%cGsXkn8vruWl4$Lo5^qHX!&Z#eHduiANIwG)vQj)uN zO}?L|n_5_qpmQ3nq$oJ|?VlVTo0^+SND@c$kwj+A&&<5v4b>Lih&TIo4Gs^Ng=~nC zC!U_3f>S^l85Hp{kmuk|h`a=V9%4LY_wNp06PZ<#5i+a8dDfXQcvTHB1BjSVH8_sQ<(PdU|@m4xQE zKb6r@mRh15_#i-l5H@mIX~{wsa-k(6(=U%;E~5BCj72He1k&E{d&?H^%Zz`o^Z$U_ z7)uYykjJQ%*bpIksk5+MEolYy8M0#WZxpJQg(OD@2lshP$rkNL`ROs^+F(RW_1ULK zL_|Q8wVj>Zvhd{W%!~wid_f`wNIpfbXg}rqJrTw(EIC5Y+!*poLxN&ShK|<1rDfwq zS_9WX7*!1oB7PSwh$tyeF<-2x2hWfe3BM0)jK0nYYS^pnY0j&Oiun9vgQ%nbA(8&a z1ts=h&9Ib|l;vd|+j25k%6Wf$zgNHJ;N&uxKGVOKWVNZxBMLA(H&4O(naui08#jy7 zBUmpPB-}I%44A;N@p0E0_W$;#rlp4)VjsSpc?QjzF;rws>5BU!&-^BCX$CcT225#& zSx;y2`ucjuEglEG2mEcX!%Zlq2dE|XL`aub%!(SA$fxDEACpl+!!2v*D7#ACnHb3I zAq<5t>-5@_>us#K{35RUhQv$a&yl0;dUf4+I8*@fB4$d0zUgh2PsfKt!aCpV{E{Cz z-!!Jdo;SE-AU05+)y5#3=|t~q!bk~zD{Vw}ba)TP#qx{I4_Pyzdiv@@3?&Jb+u_mG z%Taa38)eINXHA#cj(=e_i5U;;ygn z`}-fxSARZS9>#snp`o1Dgh{WhEz%JFISB0p2#_G>Bl0z#*`=EmGw<}sGyEn_9?v`G z+#&%XBtlv(Rh5-z!cKE5+xb;Cf6W~+5MV1heg8$HEMW*n@kKM@0024H7G`F5Rkut{ zdR;9oOYXmNQk&7ZNiyfB;xe*~%;uZLseKYYth(>i5#t^&p%RD3CPrLbZ))oOg~tVW zc$z-|xb(@#kY{uX3VV5ak1Cp0RaB?u&m`@dhDH~3c0#2L?Oa*gSU>u_KRL4zv!Cn| z7g07hH009O)ERpWGjesf-h`&7KL@|K$4K=XW?>}Dtu^)GRK zvxVrV|Av9R{lq4jxD0(EJlgro(+e&Z&aVlBxr~lhQbVP?r@WHt>WT{8xE!9|4J9|P zySrZ%fjdC}05>-`ARs6RT6^?NZw!Pv80*ZNdC0-`NB}+^OC6+$)#vrMzfG&1wSe7u z8XrGWgbeG632Kt_63C?ZixdGGJyn$1W4pm>_Ciq61{=XVMBbuSODjJEEUmNb9FXt7 z)&_~xVnWYvL3bTS91An^%_b7q)3Q#SmKJ1!-Q#kF=@_A(uiZW3TE-e?W~=`WTk2`p zP{|Okn+paJ^SsVZX1&Ooyd_A)kzi!#s`f%txknfCBlJuhZ1bJ+Qe16xI zJj<2cHI>o|v!J~^U2X*n!pH8Xs{glhRAgJ=W=6-@T3SA0Xeeb0P+iATh~=Tbgoj*M zTUl9^Rxi$3Z&W9X<-sbT@cDBOtSsUJ@y~ahw|bx0L@^s08ce~;kF+0=G9i9PL9`L~ z`6=r`668!eO#;mK7=VDu1EO!Q6$g$i+8cc{4vWHavMP#?F~1wr9#(tgdLrB*DoKsw z2G`1Jsnqt6^x!wj7ACYb8b1Hnzo)ltp#Mkw)qxTpKcA8C+1c67WY+Edee#Xp(EBn@ zC&A6Ww^xhE*@sxb7wbJTbOd#E^%~bd7~vAMSk{)7oA7mflH~CcBIVE_;_sjEx) zqgtIF8v(Yw+?4V9OK1vCXL>q+il|WsdYn2sx}*FjsQ>)o|2h3v`2S!0KknWM{|X9P z)S8mlhIpXQu+UV6^K)~Y+;u{Y2pQw&vWlo-5I^@(y-Eeaqo(ZMbP!f5VB2#5jr>X; z8;Q%Z3H6-9q$|qBDd2yux7i}Xc&w$SHXGm9Ww%j_IofTSvfAcZ-&Cvv1bW{t)mBy6 zJIx>>A+ZeV_s018NO`oKliv+v5=3*aj-Dh& zu;L+yO{ZRjF%3h+Fv0$edFLS%u^JY<2rdl>Pv2OJGc*P&VJuj(})Nh@uiUP(S zGOst8$Xsqcd07`YZ(G*sGODU0Qy?o75{}qt#}_nb4DJCSbZ2`RZJnqTNEuo5ldIUu zV@Z{7Z|5fiaW7u>(AR?2mzEkn4g^o}Z9n2E%!;l&1zcUDr}&%!h;tt#EC$jlx0eoK zza_S>lNp9huV?2cm3P&R@wXqyaC$csV?T3@7tE+zx_mLt`G!L6#<0*jw zwCJ5jpL~AuY%Mn5pDk7xOdsEJtfJ9l@Oblzt2k*m;s5~d?(U>xUw`Op?D04i=Hv(i zNO@>z-krV%k)HJS(P3o`3=A-&?#PYGg`qw48WVn3{|=D>s+zhyp2zAuJnn$***SFz ziUPld1wa5HPZb0sxN`m>a#+Wr)&mdhIYGk&hkAOAWYg1mzA)Zh^|vrAjejN(?rb?! zus%+lE*NZQU;uk;3FJ%~G?>boN$!mke&+k94e1Iw&4z&yh)2Dx^%v_Mgu*|!r}Fv= zRjuTlRJZ%WC^6`KOke>bFGuJjz&1EG#)#SjzeuR5+2kIy%Z& zL|nZmwqu~CrY6Wt^o4N2BtL~o+l8E6l@`m~!h%V^9UZvZWHy`C^^qV$`Sj!|3RUpr z)2U96V$e)Yca(}N zES@9mPn|6HZfPO&>S~+o&1}E*;*|AHRws| zWSKqkWC~cZgE(tQBIf&dH8mtaZc$}cYDR|R*)rJOOy2x6fRy5GuRR19^>Ryw5q^Ap ztW~B`FbH9?y5GRMn#^1I18{H&!=~`?gVGNl&;U+OP61h20xmD)K#`hecf9j6UG>}I zrlu5a)$MP+v5?q60mM5&mDSZsdCCHCXq8pv&;aC{YsD6~s;P*QTR!&81^kDR5Vt|wojYiefxdh-gZ%C=XTCj3{gTBgoChZQK3 zBv%Lk*buI?ECJ^=G$0Hcl4IiM=ZCyD#4||bM}R<2|5*R=NCguwo9w`>$q$Ck! zrBYCo!$N?KRZS4GL<`KVuAba;sva1$TdqSvD)}_tH3=szO*1ysGd$Df?>|-tVM2X7 zN}m)^n3N6D<2^sBf3`*2Usu=ArMV#xKgtB^V4kakoqg&$tH zL;@8ZNCFzLIY}s~!W4m8^@kGU^M8ppH{#Mmw-#6#Q}%`Tvu|#K{`W3F4*v`t&Z?F* zIKw!KjU^wJaWpIPR-T;ttEDRj#s{7?s*unU-@yP!})lc!n8H_GP3x)v){wCZOq zZpJ^`G0BC*c-~?+2=#KikI@<%9~p^|mPMP!v-9e2F1qF(_0ZCh>SSy}cNW zv9Y!$N2oJ-cqoe{QTmGOcswT?<-JT){ewR%rtAd*a%0fG219RdZpu(6q7n1^P>>mI zx74QY7}t`g>zw^Lw6LobBZmS&mOV%iKkp0(wfH;SOytyn3(JZmxe6fykaIb^1OEQ} z$x2nZ8zPp5rOy7gf$cWVrl#ifI!&h^15`XcTP-FGdX0mBfW2JBHkW#X7KMa5)6VS7 z=H0I!yCG&eQIb${K+2;|QrvXcD+3#fo@bDopqqYW9E nr4Hx8^Q> Date: Thu, 22 Nov 2018 08:30:28 +0100 Subject: [PATCH 018/258] mypy: Add to tox --- misc/requirements/requirements-mypy.txt | 5 +++++ misc/requirements/requirements-mypy.txt-raw | 1 + tox.ini | 10 ++++++++++ 3 files changed, 16 insertions(+) create mode 100644 misc/requirements/requirements-mypy.txt create mode 100644 misc/requirements/requirements-mypy.txt-raw diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt new file mode 100644 index 000000000..3a6548040 --- /dev/null +++ b/misc/requirements/requirements-mypy.txt @@ -0,0 +1,5 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +mypy==0.641 +mypy-extensions==0.4.1 +typed-ast==1.1.0 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw new file mode 100644 index 000000000..f0aa93ac8 --- /dev/null +++ b/misc/requirements/requirements-mypy.txt-raw @@ -0,0 +1 @@ +mypy diff --git a/tox.ini b/tox.ini index 8e9a54f11..4b80e8dde 100644 --- a/tox.ini +++ b/tox.ini @@ -188,3 +188,13 @@ deps = whitelist_externals = eslint changedir = {toxinidir}/qutebrowser/javascript commands = eslint --color --report-unused-disable-directives . + +[testenv:mypy] +basepython = {env:PYTHON:python3} +passenv = +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-mypy.txt +commands = + #{envpython} -m mypy --ignore-missing-imports --allow-untyped-decorators --allow-subclassing-any --strict src + {envpython} -m mypy qutebrowser From 0999945af446131604f4dd9c76f5b93dcc867573 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 13:48:34 +0100 Subject: [PATCH 019/258] mypy: Run with --ignore-missing-imports --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 4b80e8dde..9deaa64b0 100644 --- a/tox.ini +++ b/tox.ini @@ -196,5 +196,4 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt commands = - #{envpython} -m mypy --ignore-missing-imports --allow-untyped-decorators --allow-subclassing-any --strict src - {envpython} -m mypy qutebrowser + {envpython} -m mypy --ignore-missing-imports qutebrowser From 4d1b3df5e0156f90d0d7c80380347b5d38ce73c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 13:51:43 +0100 Subject: [PATCH 020/258] mypy: Fix logging.VDEBUG issues --- qutebrowser/utils/log.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 381e9ca5d..a1c4f74b7 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -76,12 +76,13 @@ LOG_COLORS = { # We first monkey-patch logging to support our VDEBUG level before getting the # loggers. Based on http://stackoverflow.com/a/13638084 +# mypy doesn't know about this, so we need to ignore it. VDEBUG_LEVEL = 9 logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG') -logging.VDEBUG = VDEBUG_LEVEL +logging.VDEBUG = VDEBUG_LEVEL # type: ignore LOG_LEVELS = { - 'VDEBUG': logging.VDEBUG, + 'VDEBUG': logging.VDEBUG, # type: ignore 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, @@ -89,17 +90,6 @@ LOG_LEVELS = { 'CRITICAL': logging.CRITICAL, } -LOGGER_NAMES = [ - 'statusbar', 'completion', 'init', 'url', - 'destroy', 'modes', 'webview', 'misc', - 'mouse', 'procs', 'hints', 'keyboard', - 'commands', 'signals', 'downloads', - 'js', 'qt', 'rfc6266', 'ipc', 'shlexer', - 'save', 'message', 'config', 'sessions', - 'webelem', 'prompt', 'network', 'sql', - 'greasemonkey' -] - def vdebug(self, msg, *args, **kwargs): """Log with a VDEBUG level. @@ -114,7 +104,7 @@ def vdebug(self, msg, *args, **kwargs): # pylint: enable=protected-access -logging.Logger.vdebug = vdebug +logging.Logger.vdebug = vdebug # type: ignore # The different loggers used. @@ -148,6 +138,17 @@ network = logging.getLogger('network') sql = logging.getLogger('sql') greasemonkey = logging.getLogger('greasemonkey') +LOGGER_NAMES = [ + 'statusbar', 'completion', 'init', 'url', + 'destroy', 'modes', 'webview', 'misc', + 'mouse', 'procs', 'hints', 'keyboard', + 'commands', 'signals', 'downloads', + 'js', 'qt', 'rfc6266', 'ipc', 'shlexer', + 'save', 'message', 'config', 'sessions', + 'webelem', 'prompt', 'network', 'sql', + 'greasemonkey' +] + ram_handler = None console_handler = None From 97d0cff93b452de34f5d415ac9c7ccf9d812a01e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 13:54:17 +0100 Subject: [PATCH 021/258] mypy: Ignore ImportError handling See https://github.com/python/mypy/issues/1153 --- qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/misc/checkpyver.py | 8 ++++---- qutebrowser/misc/earlyinit.py | 2 +- qutebrowser/qt.py | 2 +- qutebrowser/utils/qtutils.py | 2 +- qutebrowser/utils/version.py | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 0fa9366a6..b78404de9 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -37,7 +37,7 @@ try: import secrets except ImportError: # New in Python 3.6 - secrets = None + secrets = None # type: ignore from PyQt5.QtCore import QUrlQuery, QUrl, qVersion diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 50330ef88..cf8e13810 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -30,12 +30,12 @@ try: except ImportError: # pragma: no cover try: # Python2 - from Tkinter import Tk - import tkMessageBox as messagebox + from Tkinter import Tk # type: ignore + import tkMessageBox as messagebox # type: ignore except ImportError: # Some Python without Tk - Tk = None - messagebox = None + Tk = None # type: ignore + messagebox = None # type: ignore # First we check the version of Python. This code should run fine with python2 diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index b29e1508f..38dffa691 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -38,7 +38,7 @@ import datetime try: import tkinter except ImportError: - tkinter = None + tkinter = None # type: ignore # NOTE: No qutebrowser or PyQt import should be done here, as some early # initialization needs to take place before that! diff --git a/qutebrowser/qt.py b/qutebrowser/qt.py index 2878bbe98..d9f1dc58d 100644 --- a/qutebrowser/qt.py +++ b/qutebrowser/qt.py @@ -25,4 +25,4 @@ try: from PyQt5 import sip except ImportError: - import sip + import sip # type: ignore diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index c634eb95f..a7c30919f 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -39,7 +39,7 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover - qWebKitVersion = None + qWebKitVersion = None # type: ignore MAXVALS = { diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 7a99a4b65..a52e31ed8 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -42,12 +42,12 @@ from PyQt5.QtWidgets import QApplication try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover - qWebKitVersion = None + qWebKitVersion = None # type: ignore try: from PyQt5.QtWebEngineWidgets import QWebEngineProfile except ImportError: # pragma: no cover - QWebEngineProfile = None + QWebEngineProfile = None # type: ignore import qutebrowser from qutebrowser.utils import log, utils, standarddir, usertypes, message From 563e1e829484ef68bc80b7fe3edf890fc9b7f2a7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 13:56:30 +0100 Subject: [PATCH 022/258] mypy: Ignore yaml.CSafe{Loader,Dumper} See https://github.com/python/typeshed/blob/master/third_party/2and3/yaml/__init__.pyi#L9 --- qutebrowser/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 6119675ba..18dce2b05 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -41,7 +41,8 @@ from PyQt5.QtWidgets import QApplication import pkg_resources import yaml try: - from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper + from yaml import (CSafeLoader as YamlLoader, # type: ignore + CSafeDumper as YamlDumper) YAML_C_EXT = True except ImportError: # pragma: no cover from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper From a1e83f07735c293a59643ef9acd8bbf1a67253f1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:07:41 +0100 Subject: [PATCH 023/258] mypy: Use class-based API for enum.IntEnum See https://github.com/python/mypy/issues/4865 --- qutebrowser/browser/downloads.py | 6 +++++- qutebrowser/mainwindow/tabwidget.py | 7 +++++-- qutebrowser/misc/backendproblem.py | 12 ++++++++---- qutebrowser/misc/crashdialog.py | 8 ++++++-- qutebrowser/utils/usertypes.py | 15 +++++++++++---- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 92d846bd8..9c2a88a87 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -40,7 +40,11 @@ from qutebrowser.utils import (usertypes, standarddir, utils, message, log, from qutebrowser.qt import sip -ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole) +class ModelRole(enum.IntEnum): + + """Custom download model roles.""" + + item = Qt.UserRole # Remember the last used directory diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index f2605e7d3..f9c2ac0e6 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -37,8 +37,11 @@ from qutebrowser.misc import objects from qutebrowser.browser import browsertab -PixelMetrics = enum.IntEnum('PixelMetrics', ['icon_padding'], - start=QStyle.PM_CustomBase) +class PixelMetrics(enum.IntEnum): + + """Custom PixelMetrics attributes.""" + + icon_padding = QStyle.PM_CustomBase class TabWidget(QTabWidget): diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index d5f7c9680..8e57a0223 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -38,10 +38,14 @@ from qutebrowser.utils import usertypes, objreg, version, qtutils, log, utils from qutebrowser.misc import objects, msgbox -_Result = enum.IntEnum( - '_Result', - ['quit', 'restart', 'restart_webkit', 'restart_webengine'], - start=QDialog.Accepted + 1) +class _Result(enum.IntEnum): + + """The result code returned by the backend problem dialog.""" + + quit = QDialog.Accepted + 1 + restart = QDialog.Accepted + 2 + restart_webkit = QDialog.Accepted + 3 + restart_webengine = QDialog.Accepted + 4 @attr.s diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 27dec3345..a846cc59a 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -42,8 +42,12 @@ from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, from qutebrowser.config import config, configfiles -Result = enum.IntEnum('Result', ['restore', 'no_restore'], - start=QDialog.Accepted + 1) +class Result(enum.IntEnum): + + """The result code returned by the crash dialog.""" + + restore = QDialog.Accepted + 1 + no_restore = QDialog.Accepted + 2 def parse_fatal_stacktrace(text): diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 039d805f9..82a95c3fd 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -221,10 +221,17 @@ KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', 'jump_mark', 'record_macro', 'run_macro']) -# Exit statuses for errors. Needs to be an int for sys.exit. -Exit = enum.IntEnum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', - 'err_init', 'err_config', 'err_key_config'], - start=0) +class Exit(enum.IntEnum): + + """Exit statuses for errors. Needs to be an int for sys.exit.""" + + ok = 0 + reserved = 1 + exception = 2 + err_ipc = 3 + err_init = 4 + err_config = 5 + err_key_config = 6 # Load status of a tab From b37dbc45723bee27a5ba0d5e7cfea1c64e0cc1f2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:19:31 +0100 Subject: [PATCH 024/258] mypy: Add missing annotations --- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/webkit/network/networkmanager.py | 4 +++- qutebrowser/commands/cmdutils.py | 9 +++------ qutebrowser/config/configcache.py | 4 +++- qutebrowser/config/configtypes.py | 5 +++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index ceafbc011..e94e55c3d 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -645,7 +645,7 @@ class CommandDispatcher: inc_or_dec='decrement'), 'increment': functools.partial(navigate.incdec, inc_or_dec='increment'), - } + } # type: typing.Dict[str, typing.Callable] try: if where in ['prev', 'next']: diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 8d2523456..31e9e815f 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -21,6 +21,7 @@ import collections import html +import typing import attr from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, @@ -28,6 +29,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from qutebrowser.config import config +from qutebrowser.mainwindow import prompt from qutebrowser.utils import (message, log, usertypes, utils, objreg, urlutils, debug) from qutebrowser.browser import shared @@ -37,7 +39,7 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' -_proxy_auth_cache = {} +_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo] @attr.s(frozen=True) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index f9ce91b8f..768cc430e 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -17,18 +17,15 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Contains various command utils and a global command dict. - -Module attributes: - cmd_dict: A mapping from command-strings to command objects. -""" +"""Contains various command utils and a global command dict.""" import inspect +import typing from qutebrowser.utils import qtutils, log from qutebrowser.commands import command, cmdexc -cmd_dict = {} +cmd_dict = {} # type: typing.Dict[str, command.Command] def check_overflow(arg, ctype): diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py index dfead6664..cdba6456a 100644 --- a/qutebrowser/config/configcache.py +++ b/qutebrowser/config/configcache.py @@ -20,6 +20,8 @@ """Implementation of a basic config cache.""" +import typing + from qutebrowser.config import config @@ -36,7 +38,7 @@ class ConfigCache: """ def __init__(self) -> None: - self._cache = {} + self._cache = {} # type: typing.Dict[str, typing.Any] config.instance.changed.connect(self._on_config_changed) def _on_config_changed(self, attr: str) -> None: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 5503ea4f3..0314805c0 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -52,6 +52,7 @@ import datetime import functools import operator import json +import typing import attr import yaml @@ -304,7 +305,7 @@ class MappingType(BaseType): MAPPING: The mapping to use. """ - MAPPING = {} + MAPPING = {} # type: typing.Dict[str, typing.Any] def __init__(self, none_ok=False, valid_values=None): super().__init__(none_ok) @@ -576,7 +577,7 @@ class FlagList(List): the valid values of the setting. """ - combinable_values = None + combinable_values = None # type: typing.Optional[typing.Iterable] _show_valtype = False From 052c3f92ada0ad3a09aa46dd1fcc335baa2eb496 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:34:57 +0100 Subject: [PATCH 025/258] mypy: Fix issues with config default values --- qutebrowser/config/config.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1f2e45741..41309d352 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -22,6 +22,7 @@ import copy import contextlib import functools +import typing from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject @@ -30,11 +31,15 @@ from qutebrowser.utils import utils, log, jinja from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils +MYPY = False +if MYPY: + from qutebrowser.config import configcache + # An easy way to access the config from other code via config.val.foo -val = None -instance = None -key_instance = None -cache = None +val = typing.cast('ConfigContainer', None) +instance = typing.cast('Config', None) +key_instance = typing.cast('KeyConfig', None) +cache = typing.cast('configcache.ConfigCache', None) # Keeping track of all change filters to validate them later. change_filters = [] From c2c3f68828556063c6f1570cfb213ce25728a482 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:37:15 +0100 Subject: [PATCH 026/258] mypy: Move tabbed_browser --- qutebrowser/commands/runners.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index c3f5d87a1..f1c7641e7 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -58,6 +58,9 @@ def _current_url(tabbed_browser): def replace_variables(win_id, arglist): """Utility function to replace variables like {url} in a list of args.""" + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + variables = { 'url': lambda: _current_url(tabbed_browser).toString( QUrl.FullyEncoded | QUrl.RemovePassword), @@ -67,13 +70,13 @@ def replace_variables(win_id, arglist): 'clipboard': utils.get_clipboard, 'primary': lambda: utils.get_clipboard(selection=True), } + for key in list(variables): modified_key = '{' + key + '}' variables[modified_key] = lambda x=modified_key: x + values = {} args = [] - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) def repl_cb(matchobj): """Return replacement for given match.""" From b63ed090d8e1c21d9bd57aa8a6d875d48e23ae97 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:43:23 +0100 Subject: [PATCH 027/258] mypy: Fix log handler access --- qutebrowser/misc/utilcmds.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index d108a56ac..b8d8be447 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -312,6 +312,7 @@ def log_capacity(capacity: int): if capacity < 0: raise cmdexc.CommandError("Can't set a negative log capacity!") else: + assert log.ram_handler is not None log.ram_handler.change_log_capacity(capacity) @@ -326,6 +327,7 @@ def debug_log_level(level: str): level: The log level to set. """ log.change_console_formatter(log.LOG_LEVELS[level.upper()]) + assert log.console_handler is not None log.console_handler.setLevel(log.LOG_LEVELS[level.upper()]) From 08bd47bc16a27c46891b773efcaaec0a0e34d1cb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:46:22 +0100 Subject: [PATCH 028/258] mypy: Fix type for :scroll --- qutebrowser/browser/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e94e55c3d..0bb3d076b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -681,7 +681,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def scroll(self, direction: typing.Union[str, int], count=1): + def scroll(self, direction: str, count=1): """Scroll the current tab in the given direction. Note you can use `:run-with-count` to have a keybinding with a bigger From 923fd893230a04e733d49513a607f71f1c24c932 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:48:30 +0100 Subject: [PATCH 029/258] mypy: Fix int-issues in commands.py --- qutebrowser/browser/commands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0bb3d076b..2af938fb5 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -75,7 +75,7 @@ class CommandDispatcher: new_window.show() return new_window.tabbed_browser - def _count(self): + def _count(self) -> int: """Convenience method to get the widget count.""" return self._tabbed_browser.widget.count() @@ -1143,6 +1143,8 @@ class CommandDispatcher: self.tab_next() return + assert isinstance(index, int) + if index < 0: index = self._count() + index + 1 @@ -1184,6 +1186,7 @@ class CommandDispatcher: if config.val.tabs.wrap: new_idx %= self._count() else: + assert isinstance(index, int) # absolute moving if count is not None: new_idx = count - 1 From 808309196711d77efd5e0e65e1eda31747a2b985 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:53:30 +0100 Subject: [PATCH 030/258] mypy: Fix _color_flag init --- qutebrowser/mainwindow/statusbar/bar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index c3ef53b1b..3edb8128a 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -145,7 +145,7 @@ class StatusBar(QWidget): resized = pyqtSignal('QRect') moved = pyqtSignal('QPoint') _severity = None - _color_flags = [] + _color_flags = None STYLESHEET = _generate_stylesheet() From 8b1fd83366eeae0a11146bf50291a66362063a3c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 14:54:36 +0100 Subject: [PATCH 031/258] mypy: Fix AbstractSettings default values --- qutebrowser/config/websettings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index fb80c543b..659f793f8 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -19,6 +19,8 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" +import typing + from PyQt5.QtGui import QFont from qutebrowser.config import config, configutils @@ -44,10 +46,10 @@ class AbstractSettings: """Abstract base class for settings set via QWeb(Engine)Settings.""" - _ATTRIBUTES = None - _FONT_SIZES = None - _FONT_FAMILIES = None - _FONT_TO_QFONT = None + _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo] + _FONT_SIZES = {} # type: typing.Dict[str, typing.Any] + _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any] + _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint] def __init__(self, settings): self._settings = settings From 12b26512fc511518d3b37d37eb78b406991e08d2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Nov 2018 15:02:41 +0100 Subject: [PATCH 032/258] mypy: Fix :session-save We use a sentinel value for the argument so we can check whether the default was used. To express that in the type system, it needs a separate class. --- qutebrowser/misc/sessions.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 78adeb983..9d3736cdb 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -23,6 +23,7 @@ import os import os.path import itertools import urllib +import typing from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer from PyQt5.QtWidgets import QApplication @@ -37,7 +38,12 @@ from qutebrowser.mainwindow import mainwindow from qutebrowser.qt import sip -default = object() # Sentinel value +class Sentinel: + + pass + + +default = Sentinel() def init(parent=None): @@ -109,7 +115,7 @@ class SessionManager(QObject): def __init__(self, base_path, parent=None): super().__init__(parent) - self._current = None + self._current = None # type: typing.Optional[str] self._base_path = base_path self._last_window_session = None self.did_load = False @@ -504,8 +510,9 @@ class SessionManager(QObject): @cmdutils.argument('name', completion=miscmodels.session) @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('with_private', flag='p') - def session_save(self, name: str = default, current=False, quiet=False, - force=False, only_active_window=False, with_private=False, + def session_save(self, name: typing.Union[str, Sentinel] = default, + current=False, quiet=False, force=False, + only_active_window=False, with_private=False, win_id=None): """Save a session. @@ -518,7 +525,9 @@ class SessionManager(QObject): only_active_window: Saves only tabs of the currently active window. with_private: Include private windows. """ - if name is not default and name.startswith('_') and not force: + if (not isinstance(name, Sentinel) and + name.startswith('_') and + not force): raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: From 3cc2af909b75b2271a51ce916756a8ef6933d466 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 23 Nov 2018 19:40:12 +0100 Subject: [PATCH 033/258] mypy: Add PyQt5 stubs --- misc/requirements/requirements-mypy.txt | 3 +++ misc/requirements/requirements-mypy.txt-raw | 1 + tox.ini | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 3a6548040..3071410a5 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -2,4 +2,7 @@ mypy==0.641 mypy-extensions==0.4.1 +PyQt5==5.11.3 +PyQt5-sip==4.19.13 +PyQt5-stubs==5.11.3.0 typed-ast==1.1.0 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index f0aa93ac8..e400122f6 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -1 +1,2 @@ mypy +PyQt5-stubs diff --git a/tox.ini b/tox.ini index 9deaa64b0..cffee3f0e 100644 --- a/tox.ini +++ b/tox.ini @@ -196,4 +196,4 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt commands = - {envpython} -m mypy --ignore-missing-imports qutebrowser + {envpython} -m mypy qutebrowser From bd731593cebe12b4bf2879b1d7fbc099916f2d83 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 24 Nov 2018 20:12:43 +0100 Subject: [PATCH 034/258] mypy: Add {posargs} in tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cffee3f0e..5d956c0ec 100644 --- a/tox.ini +++ b/tox.ini @@ -196,4 +196,4 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt commands = - {envpython} -m mypy qutebrowser + {envpython} -m mypy qutebrowser {posargs} From 01b2c40272ab79906ee3a0f0dfb405933da36847 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 24 Nov 2018 20:12:59 +0100 Subject: [PATCH 035/258] Add requirements-optional.txt --- misc/requirements/requirements-optional.txt | 7 +++++++ misc/requirements/requirements-optional.txt-raw | 3 +++ tox.ini | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 misc/requirements/requirements-optional.txt create mode 100644 misc/requirements/requirements-optional.txt-raw diff --git a/misc/requirements/requirements-optional.txt b/misc/requirements/requirements-optional.txt new file mode 100644 index 000000000..6e1e8b8ff --- /dev/null +++ b/misc/requirements/requirements-optional.txt @@ -0,0 +1,7 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +colorama==0.4.0 +cssutils==1.0.2 +hunter==2.1.0 +Pympler==0.6 +six==1.11.0 diff --git a/misc/requirements/requirements-optional.txt-raw b/misc/requirements/requirements-optional.txt-raw new file mode 100644 index 000000000..a0be23733 --- /dev/null +++ b/misc/requirements/requirements-optional.txt-raw @@ -0,0 +1,3 @@ +hunter +cssutils +pympler diff --git a/tox.ini b/tox.ini index 5d956c0ec..8a4232aaa 100644 --- a/tox.ini +++ b/tox.ini @@ -194,6 +194,8 @@ basepython = {env:PYTHON:python3} passenv = deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-optional.txt + -r{toxinidir}/misc/requirements/requirements-pyqt.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt commands = {envpython} -m mypy qutebrowser {posargs} From ee1f7a51877c70d163b8d355d9dac76a328f3d55 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 17:49:25 +0100 Subject: [PATCH 036/258] mypy: Use own copy of PyQt5-stubs The one by upstream need various fixes which aren't merged yet. --- misc/requirements/requirements-mypy.txt | 2 +- misc/requirements/requirements-mypy.txt-raw | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 3071410a5..f2951fdf5 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -4,5 +4,5 @@ mypy==0.641 mypy-extensions==0.4.1 PyQt5==5.11.3 PyQt5-sip==4.19.13 -PyQt5-stubs==5.11.3.0 +-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs typed-ast==1.1.0 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index e400122f6..636ad43a4 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -1,2 +1,5 @@ mypy -PyQt5-stubs +-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5-stubs + +# remove @commit-id for scm installs +#@ replace: @.*# @wip# From 7834e3c7dde19baa7b2fc6bbcb8ec3ad416a2be4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 18:07:52 +0100 Subject: [PATCH 037/258] mypy: Add mypy.ini to ignore missing modules --- mypy.ini | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..9d810b738 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,32 @@ +[mypy] +# We also need to support 3.5, but if we'd chose that here, we'd need to deal +# with conditional imports (like secrets.py). +python_version = 3.6 + +[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 + +[mypy-hunter] +# https://github.com/ionelmc/python-hunter/issues/43 +ignore_missing_imports = True + +[mypy-pygments.*] +# https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints +ignore_missing_imports = True + +[mypy-cssutils] +# Pretty much inactive currently +ignore_missing_imports = True + +[mypy-pypeg2] +# Pretty much inactive currently +ignore_missing_imports = True + +[mypy-bdb] +# stdlib, missing in typeshed +ignore_missing_imports = True From 4b4b74679148d859cb9a148dd0016221c13a3120 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 18:22:02 +0100 Subject: [PATCH 038/258] mypy: Add type annotations for browsertab.AbstractAction --- qutebrowser/browser/browsertab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index b2be458ce..0cd54be7e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -136,8 +136,8 @@ class AbstractAction: action_base: The type of the actions (QWeb{Engine,}Page.WebAction) """ - action_class = None - action_base = None + action_class = None # type: type + action_base = None # type: type def __init__(self, tab): self._widget = None From fda807ce9a3375f63e4e226daeb15d4317e137fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 19:03:07 +0100 Subject: [PATCH 039/258] mypy: Allow trivial --strict options --- mypy.ini | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mypy.ini b/mypy.ini index 9d810b738..797ed0bca 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,6 +3,22 @@ # with conditional imports (like secrets.py). python_version = 3.6 +# --strict +warn_unused_configs = True +warn_redundant_casts = True +warn_unused_ignores = True +disallow_subclassing_any = True +# disallow_untyped_calls = True +# disallow_untyped_defs = True +# disallow_incomplete_defs = True +# check_untyped_defs = True +# disallow_untyped_decorators = True +# no_implicit_optional = True +# warn_return_any = True + +# disallow_any_generics = True + + [mypy-faulthandler] # https://github.com/python/typeshed/pull/2627 ignore_missing_imports = True @@ -30,3 +46,7 @@ ignore_missing_imports = True [mypy-bdb] # stdlib, missing in typeshed ignore_missing_imports = True + +[mypy-qutebrowser.browser.webkit.rfc6266] +# subclasses dynamic PyPEG2 classes +disallow_subclassing_any = False From a651f8b5506e1494642579a0fe8fbc37489dd84d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:14 +0100 Subject: [PATCH 040/258] Update setuptools from 40.5.0 to 40.6.2 --- 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 a3068c5f9..04b033939 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.5.0 +setuptools==40.6.2 six==1.11.0 wheel==0.32.2 From 343f3025fcf2ac2d11a296136ed6210b00550a3d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:16 +0100 Subject: [PATCH 041/258] Update wheel from 0.32.2 to 0.32.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 04b033939..43085ddd7 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -5,4 +5,4 @@ packaging==18.0 pyparsing==2.3.0 setuptools==40.6.2 six==1.11.0 -wheel==0.32.2 +wheel==0.32.3 From 4791d581f8c101f2d9f26eb0d4cd07ea18ecdb5e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:17 +0100 Subject: [PATCH 042/258] Update astroid from 2.0.4 to 2.1.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 30b038ef8..2cf83abba 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py asn1crypto==0.24.0 -astroid==2.0.4 +astroid==2.1.0 certifi==2018.10.15 cffi==1.11.5 chardet==3.0.4 From 6131cf538f063c795fe335b2654712d70fb512d4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:19 +0100 Subject: [PATCH 043/258] Update cryptography from 2.4.1 to 2.4.2 --- 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 2cf83abba..d8d090ac9 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -5,7 +5,7 @@ astroid==2.1.0 certifi==2018.10.15 cffi==1.11.5 chardet==3.0.4 -cryptography==2.4.1 +cryptography==2.4.2 github3.py==1.2.0 idna==2.7 isort==4.3.4 From 4318dd500ab07981579049570471e9cd73390e78 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:21 +0100 Subject: [PATCH 044/258] Update pylint from 2.1.1 to 2.2.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 d8d090ac9..44c2b19e8 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -13,7 +13,7 @@ jwcrypto==0.6.0 lazy-object-proxy==1.3.1 mccabe==0.6.1 pycparser==2.19 -pylint==2.1.1 +pylint==2.2.0 python-dateutil==2.7.5 ./scripts/dev/pylint_checkers requests==2.20.1 From fe4baa8ceab885fac174254d3b3f175dfaa442a4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:22 +0100 Subject: [PATCH 045/258] Update hunter from 2.0.2 to 2.1.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 27390c85a..d6bc1820e 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -12,7 +12,7 @@ EasyProcess==0.2.3 fields==5.0.0 Flask==1.0.2 glob2==0.6 -hunter==2.0.2 +hunter==2.1.0 hypothesis==3.82.1 itsdangerous==1.1.0 # Jinja2==2.10 From 866c009677b53bf6b1ec5786d89f05ab52969642 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:24 +0100 Subject: [PATCH 046/258] Update pytest from 4.0.0 to 4.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 d6bc1820e..6ea860f19 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -24,7 +24,7 @@ parse-type==0.4.2 pluggy==0.8.0 py==1.7.0 py-cpuinfo==4.0.0 -pytest==4.0.0 +pytest==4.0.1 pytest-bdd==3.0.0 pytest-benchmark==3.1.1 pytest-cov==2.6.0 From 2cd67260fc99518315edf1bb380dafb8678a1ba5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:26 +0100 Subject: [PATCH 047/258] Update colorama from 0.4.0 to 0.4.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 022a6214f..53da22d02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py attrs==18.2.0 -colorama==0.4.0 +colorama==0.4.1 cssutils==1.0.2 Jinja2==2.10 MarkupSafe==1.1.0 From 9a3e0a34e7a83d7190fa43bebd49df875b323dbc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 26 Nov 2018 19:10:27 +0100 Subject: [PATCH 048/258] Update pygments from 2.2.0 to 2.3.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53da22d02..66dcf23ef 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.2.0 +Pygments==2.3.0 pyPEG2==2.15.2 PyYAML==3.13 From 1f36e56e1c724b9a7a4ef4236774b8e1379a1351 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 20:12:03 +0100 Subject: [PATCH 049/258] Complete partial annotations Unfortunately we can't turn on mypy's --disallow-incomplete-defs (yet) due to https://github.com/python/mypy/issues/5954 --- mypy.ini | 1 + qutebrowser/browser/browsertab.py | 4 +-- qutebrowser/browser/commands.py | 28 +++++++++++-------- qutebrowser/browser/downloads.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webengine/webview.py | 2 +- qutebrowser/browser/webkit/webkittab.py | 2 +- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/config/configcache.py | 2 +- qutebrowser/mainwindow/prompt.py | 3 +- qutebrowser/mainwindow/tabwidget.py | 8 +++--- qutebrowser/misc/sessions.py | 9 ++++-- qutebrowser/misc/utilcmds.py | 13 +++++---- 13 files changed, 44 insertions(+), 34 deletions(-) diff --git a/mypy.ini b/mypy.ini index 797ed0bca..91c314675 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,6 +10,7 @@ warn_unused_ignores = True disallow_subclassing_any = True # disallow_untyped_calls = True # disallow_untyped_defs = True +# https://github.com/python/mypy/issues/5954 # disallow_incomplete_defs = True # check_untyped_defs = True # disallow_untyped_decorators = True diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 0cd54be7e..02d9b70dd 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -685,7 +685,7 @@ class AbstractAudio(QObject): self._widget = None self._tab = tab - def set_muted(self, muted: bool, override: bool = False): + def set_muted(self, muted: bool, override: bool = False) -> None: """Set this tab as muted or not. Arguments: @@ -699,7 +699,7 @@ class AbstractAudio(QObject): """Whether this tab is muted.""" raise NotImplementedError - def toggle_muted(self, *, override: bool = False): + def toggle_muted(self, *, override: bool = False) -> None: self.set_muted(not self.is_muted(), override=override) def is_recently_audible(self): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2af938fb5..5c97aaf53 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -513,7 +513,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('win_id', completion=miscmodels.window) @cmdutils.argument('count', count=True) - def tab_give(self, win_id: int = None, keep=False, count=None): + def tab_give(self, win_id: int = None, keep: bool = False, + count: int = None) -> None: """Give the current tab to a new or existing window if win_id given. If no win_id is given, the tab will get detached into a new window. @@ -601,7 +602,8 @@ class CommandDispatcher: @cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment', 'decrement']) @cmdutils.argument('count', count=True) - def navigate(self, where: str, tab=False, bg=False, window=False, count=1): + def navigate(self, where: str, tab: bool = False, bg: bool = False, + window: bool = False, count: int = 1) -> None: """Open typical prev/next links or navigate using the URL path. This tries to automatically click on typical _Previous Page_ or @@ -665,7 +667,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def scroll_px(self, dx: int, dy: int, count=1): + def scroll_px(self, dx: int, dy: int, count: int = 1) -> None: """Scroll the current tab by 'count * dx/dy' pixels. Args: @@ -681,7 +683,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def scroll(self, direction: str, count=1): + def scroll(self, direction: str, count: int = 1) -> None: """Scroll the current tab in the given direction. Note you can use `:run-with-count` to have a keybinding with a bigger @@ -719,7 +721,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @cmdutils.argument('horizontal', flag='x') - def scroll_to_perc(self, perc: float = None, horizontal=False, count=None): + def scroll_to_perc(self, perc: float = None, horizontal: bool = False, + count: int = None) -> None: """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. @@ -764,7 +767,7 @@ class CommandDispatcher: choices=('next', 'increment')) def scroll_page(self, x: float, y: float, *, top_navigate: str = None, bottom_navigate: str = None, - count=1): + count: int = 1) -> None: """Scroll the frame page-wise. Args: @@ -1120,7 +1123,7 @@ class CommandDispatcher: @cmdutils.argument('index', choices=['last']) @cmdutils.argument('count', count=True) def tab_focus(self, index: typing.Union[str, int] = None, - count=None, no_last=False): + count: int = None, no_last: bool = False) -> None: """Select the tab given as argument/[count]. If neither count nor index are given, it behaves like tab-next. @@ -1161,7 +1164,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['+', '-']) @cmdutils.argument('count', count=True) - def tab_move(self, index: typing.Union[str, int] = None, count=None): + def tab_move(self, index: typing.Union[str, int] = None, + count: int = None) -> None: """Move the current tab according to the argument and [count]. If neither is given, move it to the first position. @@ -1718,10 +1722,10 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('filter_', choices=['id']) - def click_element(self, filter_: str, value, *, + def click_element(self, filter_: str, value: str, *, target: usertypes.ClickTarget = usertypes.ClickTarget.normal, - force_event=False): + force_event: bool = False) -> None: """Click the element matching the given filter. The given filter needs to result in exactly one element, otherwise, an @@ -2070,8 +2074,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) - def jseval(self, js_code, file=False, quiet=False, *, - world: typing.Union[usertypes.JsWorld, int] = None): + def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *, + world: typing.Union[usertypes.JsWorld, int] = None) -> None: """Evaluate a JavaScript string. Args: diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 9c2a88a87..1709c7425 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1062,7 +1062,7 @@ class DownloadModel(QAbstractListModel): @cmdutils.register(instance='download-model', scope='window', maxsplit=0) @cmdutils.argument('count', count=True) - def download_open(self, cmdline: str = None, count=0): + def download_open(self, cmdline: str = None, count: int = 0) -> None: """Open the last/[count]th download. If no specific command is given, this will use the system's default diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 0f1d368e9..9945886fa 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -670,7 +670,7 @@ class WebEngineAudio(browsertab.AbstractAudio): self._tab.url_changed.connect(self._on_url_changed) config.instance.changed.connect(self._on_config_changed) - def set_muted(self, muted: bool, override: bool = False): + def set_muted(self, muted: bool, override: bool = False) -> None: self._overridden = override page = self._widget.page() page.setAudioMuted(muted) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index b10cc5f9a..e70226f30 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -240,7 +240,7 @@ class WebEnginePage(QWebEnginePage): def acceptNavigationRequest(self, url: QUrl, typ: QWebEnginePage.NavigationType, - is_main_frame: bool): + is_main_frame: bool) -> bool: """Override acceptNavigationRequest to forward it to the tab API.""" type_map = { QWebEnginePage.NavigationTypeLinkClicked: diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 2edea1777..c791326ce 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -641,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio): """Dummy handling of audio status for QtWebKit.""" - def set_muted(self, muted: bool, override: bool = False): + def set_muted(self, muted: bool, override: bool = False) -> None: raise browsertab.WebTabError('Muting is not supported on QtWebKit!') def is_muted(self): diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index ce985b466..0195ec17f 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -469,7 +469,7 @@ class BrowserPage(QWebPage): def acceptNavigationRequest(self, frame: QWebFrame, request: QNetworkRequest, - typ: QWebPage.NavigationType): + typ: QWebPage.NavigationType) -> bool: """Override acceptNavigationRequest to handle clicked links. Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py index cdba6456a..a421ba85c 100644 --- a/qutebrowser/config/configcache.py +++ b/qutebrowser/config/configcache.py @@ -45,7 +45,7 @@ class ConfigCache: if attr in self._cache: self._cache[attr] = config.instance.get(attr) - def __getitem__(self, attr: str): + def __getitem__(self, attr: str) -> typing.Any: if attr not in self._cache: assert not config.instance.get_opt(attr).supports_pattern self._cache[attr] = config.instance.get(attr) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 5eb76c86e..f6a8b1224 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -391,7 +391,8 @@ class PromptContainer(QWidget): @cmdutils.register(instance='prompt-container', scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) - def prompt_open_download(self, cmdline: str = None, pdfjs=False): + def prompt_open_download(self, cmdline: str = None, + pdfjs: bool = False) -> None: """Immediately open a download. If no specific command is given, this will use the system's default diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index f9c2ac0e6..a3ba0f1da 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -342,7 +342,7 @@ class TabWidget(QTabWidget): qtutils.ensure_valid(url) return url - def update_tab_favicon(self, tab: QWidget): + def update_tab_favicon(self, tab: QWidget) -> None: """Update favicon of the given tab.""" idx = self.indexOf(tab) @@ -400,7 +400,7 @@ class TabBar(QTabBar): return self.parent().currentWidget() @pyqtSlot(str) - def _on_config_changed(self, option: str): + def _on_config_changed(self, option: str) -> None: if option == 'fonts.tabs': self._set_font() elif option == 'tabs.favicons.scale': @@ -543,7 +543,7 @@ class TabBar(QTabBar): return super().mousePressEvent(e) - def minimumTabSizeHint(self, index, ellipsis: bool = True) -> QSize: + def minimumTabSizeHint(self, index: int, ellipsis: bool = True) -> QSize: """Set the minimum tab size to indicator/icon/... text. Args: @@ -623,7 +623,7 @@ class TabBar(QTabBar): return False return widget.data.pinned - def tabSizeHint(self, index: int): + def tabSizeHint(self, index: int) -> QSize: """Override tabSizeHint to customize qb's tab size. https://wiki.python.org/moin/PyQt/Customising%20tab%20bars diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 9d3736cdb..b94566830 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -511,9 +511,12 @@ class SessionManager(QObject): @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('with_private', flag='p') def session_save(self, name: typing.Union[str, Sentinel] = default, - current=False, quiet=False, force=False, - only_active_window=False, with_private=False, - win_id=None): + current: bool = False, + quiet: bool = False, + force: bool = False, + only_active_window: bool = False, + with_private: bool = False, + win_id: int = None) -> None: """Save a session. Args: diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index b8d8be447..b893c8d6e 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -44,7 +44,7 @@ from qutebrowser.qt import sip @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('win_id', win_id=True) -def later(ms: int, command, win_id): +def later(ms: int, command: str, win_id: int) -> None: """Execute a command after some time. Args: @@ -75,7 +75,7 @@ def later(ms: int, command, win_id): @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('count', count=True) -def repeat(times: int, command, win_id, count=None): +def repeat(times: int, command: str, win_id: int, count: int = None) -> None: """Repeat a given command. Args: @@ -96,7 +96,8 @@ def repeat(times: int, command, win_id, count=None): @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('count', count=True) -def run_with_count(count_arg: int, command, win_id, count=1): +def run_with_count(count_arg: int, command: str, win_id: int, + count: int = 1) -> None: """Run a command with the given count. If run_with_count itself is run with a count, it multiplies count_arg. @@ -303,7 +304,7 @@ def repeat_command(win_id, count=None): @cmdutils.register(debug=True, name='debug-log-capacity') -def log_capacity(capacity: int): +def log_capacity(capacity: int) -> None: """Change the number of log lines to be stored in RAM. Args: @@ -320,7 +321,7 @@ def log_capacity(capacity: int): @cmdutils.argument('level', choices=sorted( (level.lower() for level in log.LOG_LEVELS), key=lambda e: log.LOG_LEVELS[e.upper()])) -def debug_log_level(level: str): +def debug_log_level(level: str) -> None: """Change the log level for console logging. Args: @@ -332,7 +333,7 @@ def debug_log_level(level: str): @cmdutils.register(debug=True) -def debug_log_filter(filters: str): +def debug_log_filter(filters: str) -> None: """Change the log filter for console logging. Args: From 6b5a92fb2d30ad76bf73b0b065da9d0384157209 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:24:31 +0100 Subject: [PATCH 050/258] mypy: Update config --- mypy.ini | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 91c314675..e002720d3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,16 +10,13 @@ warn_unused_ignores = True disallow_subclassing_any = True # disallow_untyped_calls = True # disallow_untyped_defs = True -# https://github.com/python/mypy/issues/5954 +## https://github.com/python/mypy/issues/5954 # disallow_incomplete_defs = True # check_untyped_defs = True # disallow_untyped_decorators = True # no_implicit_optional = True # warn_return_any = True -# disallow_any_generics = True - - [mypy-faulthandler] # https://github.com/python/typeshed/pull/2627 ignore_missing_imports = True From d2751935e0635a1d9f8ce7657ce307d1dd63267e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:24:41 +0100 Subject: [PATCH 051/258] Convert *args to list It being a tuple also happens to work, but is somewhat inconsistent. --- qutebrowser/browser/hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index d8d5a0624..aa5b5f34c 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -737,7 +737,7 @@ class HintManager(QObject): self._context.baseurl = tabbed_browser.current_url() except qtutils.QtValueError: raise cmdexc.CommandError("No URL set for this page yet!") - self._context.args = args + self._context.args = list(args) self._context.group = group try: From 462e07a578c3a9e4a5e325d58d2c490db1c0d6eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:25:13 +0100 Subject: [PATCH 052/258] Use integer division to set font weight QFont::setFontWeight takes an int only - it looks like PyQt accepts a float and just truncates it. That's the behaviour we actually want here, but let's be explicit about it. --- qutebrowser/config/configtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 0314805c0..38d952639 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1119,7 +1119,7 @@ class QtFont(Font): font.setWeight(weight_map[namedweight]) if weight: # based on qcssparser.cpp:setFontWeightFromValue - font.setWeight(min(int(weight) / 8, 99)) + font.setWeight(min(int(weight) // 8, 99)) if size: if size.lower().endswith('pt'): font.setPointSizeF(float(size[:-2])) From e851480a2bcee3080aad785ac4d2f7d193201cd4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:30:21 +0100 Subject: [PATCH 053/258] Use empty tuple instead of None While None works (as logging.py does "if args:" consistently), it was never intended to be used like that. --- qutebrowser/utils/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index a1c4f74b7..bbc025515 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -468,7 +468,7 @@ def qt_message_handler(msg_type, context, msg): stack = ''.join(traceback.format_stack()) else: stack = None - record = qt.makeRecord(name, level, context.file, context.line, msg, None, + record = qt.makeRecord(name, level, context.file, context.line, msg, (), None, func, sinfo=stack) qt.handle(record) From 06afd3604e4656ed9ccaceaf43cb6af9bbe11b7c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:30:55 +0100 Subject: [PATCH 054/258] Don't try to iterate over auth info This caused a crash when trying to use a proxy with authentication on QtWebKit. It was introduced in 3a5241b642da666e4517a32e0eb945254d86a6da since it was a namedtuple before and was iterable like this. --- qutebrowser/browser/webkit/network/networkmanager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 31e9e815f..4c44ba887 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -297,9 +297,9 @@ class NetworkManager(QNetworkAccessManager): """Called when a proxy needs authentication.""" proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port()) if proxy_id in _proxy_auth_cache: - user, password = _proxy_auth_cache[proxy_id] - authenticator.setUser(user) - authenticator.setPassword(password) + authinfo = _proxy_auth_cache[proxy_id] + authenticator.setUser(authinfo.user) + authenticator.setPassword(authinfo.password) else: msg = '{} says:
{}'.format( html.escape(proxy.hostName()), From f34a8ba1946901e7f484f2253789bb8f65e5217c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Nov 2018 23:32:20 +0100 Subject: [PATCH 055/258] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 41515d287..51647bafd 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -66,6 +66,7 @@ Fixed 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. +- Crash when trying to use a proxy requiring authentication with QtWebKit. v1.5.2 ------ From 7934dc9a95a26e0a23337871d94576384f18ae92 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:37:59 +0100 Subject: [PATCH 056/258] mypy: Fix assert location --- qutebrowser/browser/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 5c97aaf53..a53cfc75e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1190,11 +1190,11 @@ class CommandDispatcher: if config.val.tabs.wrap: new_idx %= self._count() else: - assert isinstance(index, int) # absolute moving if count is not None: new_idx = count - 1 elif index is not None: + assert isinstance(index, int) new_idx = index - 1 if index >= 0 else index + self._count() else: new_idx = 0 From ec0dc59b06da1074b70fbb724e090e9f4d8384e5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:39:05 +0100 Subject: [PATCH 057/258] mypy: Disable warn_unused_configs Looks like mypy's cache causes config sections to be ignored sometimes. --- mypy.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index e002720d3..578a8c1fc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4,10 +4,11 @@ python_version = 3.6 # --strict -warn_unused_configs = True warn_redundant_casts = True warn_unused_ignores = True disallow_subclassing_any = True +## https://github.com/python/mypy/issues/5957 +# warn_unused_configs = True # disallow_untyped_calls = True # disallow_untyped_defs = True ## https://github.com/python/mypy/issues/5954 From 80808ee9d242768eb2f21055f3859e4284178751 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:54:25 +0100 Subject: [PATCH 058/258] Add docstring --- qutebrowser/misc/sessions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index b94566830..e50d803a2 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -40,6 +40,8 @@ from qutebrowser.qt import sip class Sentinel: + """Sentinel value for default argument.""" + pass From c931654a8d09385f713ab09b5348a57e53901ce4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:56:51 +0100 Subject: [PATCH 059/258] Remove old exit status values Those weren't used anymore since bc8176ff21a1aab95f5ad3a34c97e1dd098a0242. Since the values were removed in v1.0.0 and never used since then, it seems fine to re-use them in the future. --- qutebrowser/utils/usertypes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 82a95c3fd..cd36db49a 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -230,8 +230,6 @@ class Exit(enum.IntEnum): exception = 2 err_ipc = 3 err_init = 4 - err_config = 5 - err_key_config = 6 # Load status of a tab From 984970e7655aff7c5f9c25673bd587bf279c8271 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:59:13 +0100 Subject: [PATCH 060/258] vulture: Ignore reserved value --- scripts/dev/run_vulture.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 0015539f9..b5c083546 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -84,6 +84,7 @@ def whitelist_generator(): # noqa yield 'qutebrowser.utils.log.QtWarningFilter.filter' yield 'qutebrowser.browser.pdfjs.is_available' yield 'qutebrowser.misc.guiprocess.spawn_output' + yield 'qutebrowser.utils.usertypes.ExitStatus.reserved' yield 'QEvent.posted' yield 'log_stack' # from message.py yield 'propagate' # logging.getLogger('...).propagate = False From 251531e6d08cfaf4f060c1a4336b2c370fb890b3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 08:59:39 +0100 Subject: [PATCH 061/258] manifest: Exclude mypy.ini --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ff96264aa..3a29ba690 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -32,6 +32,7 @@ include doc/changelog.asciidoc prune tests prune qutebrowser/3rdparty exclude pytest.ini +exclude mypy.ini exclude qutebrowser/javascript/.eslintrc.yaml exclude qutebrowser/javascript/.eslintignore exclude doc/help From 21edeca3e06548995ed117914193e1c7348deeec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 09:52:54 +0100 Subject: [PATCH 062/258] pylint: disable=unused-import for typing Apparently marking modules as used based on type comments doesn't work with Python 3.7: https://github.com/PyCQA/pylint/issues/2345 https://github.com/python/typed_ast/issues/60 --- .travis.yml | 2 +- qutebrowser/browser/webkit/network/networkmanager.py | 4 ++-- qutebrowser/commands/cmdutils.py | 2 +- qutebrowser/config/config.py | 2 +- qutebrowser/config/configtypes.py | 2 +- qutebrowser/config/websettings.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5b918a393..3212142c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ matrix: # env: TESTENV=py35 OSX=yosemite # osx_image: xcode6.4 - os: linux - env: TESTENV=pylint PYTHON=python3.6 + env: TESTENV=pylint - os: linux env: TESTENV=flake8 - os: linux diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 4c44ba887..0c3148ee4 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -21,7 +21,7 @@ import collections import html -import typing +import typing # pylint: disable=unused-import import attr from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, @@ -29,7 +29,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from qutebrowser.config import config -from qutebrowser.mainwindow import prompt +from qutebrowser.mainwindow import prompt # pylint: disable=unused-import from qutebrowser.utils import (message, log, usertypes, utils, objreg, urlutils, debug) from qutebrowser.browser import shared diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 768cc430e..41e875202 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -20,7 +20,7 @@ """Contains various command utils and a global command dict.""" import inspect -import typing +import typing # pylint: disable=unused-import from qutebrowser.utils import qtutils, log from qutebrowser.commands import command, cmdexc diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 41309d352..59df90ce8 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -33,7 +33,7 @@ from qutebrowser.keyinput import keyutils MYPY = False if MYPY: - from qutebrowser.config import configcache + from qutebrowser.config import configcache # pylint: disable=unused-import # An easy way to access the config from other code via config.val.foo val = typing.cast('ConfigContainer', None) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 38d952639..31eca988e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -52,7 +52,7 @@ import datetime import functools import operator import json -import typing +import typing # pylint: disable=unused-import import attr import yaml diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 659f793f8..e6d19db7e 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -19,7 +19,7 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" -import typing +import typing # pylint: disable=unused-import from PyQt5.QtGui import QFont From caca60087ff5a2638513641a94a9a389a0186a6c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 13:24:48 +0100 Subject: [PATCH 063/258] Fix coverage --- qutebrowser/config/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 59df90ce8..1d7e34345 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -33,7 +33,8 @@ from qutebrowser.keyinput import keyutils MYPY = False if MYPY: - from qutebrowser.config import configcache # pylint: disable=unused-import + # pylint: disable=unused-import + from qutebrowser.config import configcache # pragma: no cover # An easy way to access the config from other code via config.val.foo val = typing.cast('ConfigContainer', None) From fca2d9dfb4862e94c792b7b4f3973bf7eb71e364 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 16:15:08 +0100 Subject: [PATCH 064/258] mypy: Add to travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3212142c7..5a8965fd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,6 +41,8 @@ matrix: env: TESTENV=pylint - os: linux env: TESTENV=flake8 + - os: linux + env: TESTENV=mypy - os: linux env: TESTENV=docs addons: From 43c2cccc17b35f476d89f6569a9c25f64a0f82ff Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 17:39:52 +0100 Subject: [PATCH 065/258] travis: Set "os: linux" globally --- .travis.yml | 58 ++++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a8965fd4..62431a8e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,32 +2,25 @@ dist: xenial language: python group: edge python: 3.6 +os: linux matrix: include: - - os: linux - env: DOCKER=archlinux + - env: DOCKER=archlinux services: docker - - os: linux - env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true + - env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true services: docker - - os: linux - env: TESTENV=py36-pyqt571 - - os: linux - python: 3.5 + - env: TESTENV=py36-pyqt571 + - python: 3.5 env: TESTENV=py35-pyqt571 - - os: linux - env: TESTENV=py36-pyqt59 - - os: linux - env: TESTENV=py36-pyqt510 + - env: TESTENV=py36-pyqt59 + - env: TESTENV=py36-pyqt510 addons: apt: packages: - xfonts-base - - os: linux - env: TESTENV=py36-pyqt511-cov - - os: linux - python: 3.7 + - env: TESTENV=py36-pyqt511-cov + - python: 3.7 env: TESTENV=py37-pyqt511 - os: osx env: TESTENV=py37 OSX=sierra @@ -37,40 +30,29 @@ matrix: # - os: osx # env: TESTENV=py35 OSX=yosemite # osx_image: xcode6.4 - - os: linux - env: TESTENV=pylint - - os: linux - env: TESTENV=flake8 - - os: linux - env: TESTENV=mypy - - os: linux - env: TESTENV=docs + - env: TESTENV=pylint + - env: TESTENV=flake8 + - env: TESTENV=mypy + - env: TESTENV=docs addons: apt: packages: - asciidoc - - os: linux - env: TESTENV=vulture - - os: linux - env: TESTENV=misc - - os: linux - env: TESTENV=pyroma - - os: linux - env: TESTENV=check-manifest - - os: linux - env: TESTENV=eslint + - env: TESTENV=vulture + - env: TESTENV=misc + - env: TESTENV=pyroma + - env: TESTENV=check-manifest + - env: TESTENV=eslint language: node_js python: null node_js: "lts/*" - - os: linux - language: generic + - language: generic env: TESTENV=shellcheck services: docker fast_finish: true allow_failures: # https://github.com/qutebrowser/qutebrowser/issues/4055 - - os: linux - env: TESTENV=py36-pyqt510 + - env: TESTENV=py36-pyqt510 cache: directories: From f36a98ec76c6be2a90230a9f65fba71ab684ed27 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 17:50:41 +0100 Subject: [PATCH 066/258] travis: Reallow PyQt 5.10 Looks like this works again? Maybe because of Ubuntu Xenial? Fixes #4055 --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62431a8e9..663c4c6f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,9 +50,6 @@ matrix: env: TESTENV=shellcheck services: docker fast_finish: true - allow_failures: - # https://github.com/qutebrowser/qutebrowser/issues/4055 - - env: TESTENV=py36-pyqt510 cache: directories: From d7786c694fb30eb9da79b5842629cc271d0ed09e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Nov 2018 20:35:58 +0100 Subject: [PATCH 067/258] Add type hints for qutebrowser.browser.browsertab --- mypy.ini | 3 + qutebrowser/browser/browsertab.py | 372 ++++++++++-------- qutebrowser/browser/webengine/webenginetab.py | 9 +- qutebrowser/browser/webkit/webkittab.py | 8 +- 4 files changed, 229 insertions(+), 163 deletions(-) diff --git a/mypy.ini b/mypy.ini index 578a8c1fc..94d5dff40 100644 --- a/mypy.ini +++ b/mypy.ini @@ -49,3 +49,6 @@ ignore_missing_imports = True [mypy-qutebrowser.browser.webkit.rfc6266] # subclasses dynamic PyPEG2 classes disallow_subclassing_any = False + +[mypy-qutebrowser.browser.browsertab] +disallow_untyped_defs = True diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 02d9b70dd..f3a613844 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -21,12 +21,19 @@ import enum import itertools +import typing import attr -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt, + QEvent, QPoint) from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QApplication, QDialog -from PyQt5.QtPrintSupport import QPrintDialog +from PyQt5.QtPrintSupport import QPrintDialog, QPrinter +from PyQt5.QtNetwork import QNetworkAccessManager +MYPY = False +if MYPY: + # pylint: disable=unused-import + from PyQt5.QtWebEngineWidgets import QWebEngineView import pygments import pygments.lexers @@ -37,14 +44,19 @@ from qutebrowser.config import config from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, urlutils, message) from qutebrowser.misc import miscwidgets, objects -from qutebrowser.browser import mouse, hints +from qutebrowser.browser import mouse, hints, webelem from qutebrowser.qt import sip +if MYPY: + # pylint: disable=unused-import + from qutebrowser.browser.inspector import AbstractWebInspector tab_id_gen = itertools.count(0) -def create(win_id, private, parent=None): +def create(win_id: int, + private: bool, + parent: QWidget = None) -> 'AbstractTab': """Get a QtWebKit/QtWebEngine tab object. Args: @@ -65,7 +77,7 @@ def create(win_id, private, parent=None): parent=parent) -def init(): +def init() -> None: """Initialize backend-specific modules.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginetab @@ -112,17 +124,18 @@ class TabData: input_mode: current input mode for the tab. """ - keep_icon = attr.ib(False) - viewing_source = attr.ib(False) - inspector = attr.ib(None) - open_target = attr.ib(usertypes.ClickTarget.normal) - override_target = attr.ib(None) - pinned = attr.ib(False) - fullscreen = attr.ib(False) - netrc_used = attr.ib(False) - input_mode = attr.ib(usertypes.KeyMode.normal) + keep_icon = attr.ib(False) # type: bool + viewing_source = attr.ib(False) # type: bool + inspector = attr.ib(None) # type: typing.Optional[AbstractWebInspector] + open_target = attr.ib( + usertypes.ClickTarget.normal) # type: usertypes.ClickTarget + override_target = attr.ib(None) # type: usertypes.ClickTarget + pinned = attr.ib(False) # type: bool + fullscreen = attr.ib(False) # type: bool + netrc_used = attr.ib(False) # type: bool + input_mode = attr.ib(usertypes.KeyMode.normal) # type: usertypes.KeyMode - def should_show_icon(self): + def should_show_icon(self) -> bool: return (config.val.tabs.favicons.show == 'always' or config.val.tabs.favicons.show == 'pinned' and self.pinned) @@ -139,33 +152,35 @@ class AbstractAction: action_class = None # type: type action_base = None # type: type - def __init__(self, tab): - self._widget = None + def __init__(self, tab: 'AbstractTab') -> None: + self._widget = typing.cast('QWebEngineView', None) self._tab = tab - def exit_fullscreen(self): + def exit_fullscreen(self) -> None: """Exit the fullscreen mode.""" raise NotImplementedError - def save_page(self): + def save_page(self) -> None: """Save the current page.""" raise NotImplementedError - def run_string(self, name): + def run_string(self, name: str) -> None: """Run a webaction based on its name.""" member = getattr(self.action_class, name, None) if not isinstance(member, self.action_base): raise WebTabError("{} is not a valid web action!".format(name)) - self._widget.triggerPageAction(member) + self._widget.triggerPageAction(member) # type: ignore - def show_source(self, - pygments=False): # pylint: disable=redefined-outer-name + def show_source( + self, + pygments: bool = False # pylint: disable=redefined-outer-name + ) -> None: """Show the source of the current page in a new tab.""" raise NotImplementedError - def _show_source_pygments(self): + def _show_source_pygments(self) -> None: - def show_source_cb(source): + def show_source_cb(source: str) -> None: """Show source as soon as it's ready.""" # WORKAROUND for https://github.com/PyCQA/pylint/issues/491 # pylint: disable=no-member @@ -188,23 +203,24 @@ class AbstractPrinting: """Attribute of AbstractTab for printing the page.""" - def __init__(self, tab): + def __init__(self, tab: 'AbstractTab') -> None: self._widget = None self._tab = tab - def check_pdf_support(self): + def check_pdf_support(self) -> bool: raise NotImplementedError - def check_printer_support(self): + def check_printer_support(self) -> bool: raise NotImplementedError - def check_preview_support(self): + def check_preview_support(self) -> bool: raise NotImplementedError - def to_pdf(self, filename): + def to_pdf(self, filename: str) -> bool: raise NotImplementedError - def to_printer(self, printer, callback=None): + def to_printer(self, printer: QPrinter, + callback: typing.Callable[[bool], None] = None) -> None: """Print the tab. Args: @@ -214,17 +230,17 @@ class AbstractPrinting: """ raise NotImplementedError - def show_dialog(self): + def show_dialog(self) -> None: """Print with a QPrintDialog.""" self.check_printer_support() - def print_callback(ok): + def print_callback(ok: bool) -> None: """Called when printing finished.""" if not ok: message.error("Printing failed!") diag.deleteLater() - def do_print(): + def do_print() -> None: """Called when the dialog was closed.""" self.to_printer(diag.printer(), print_callback) @@ -257,15 +273,16 @@ class AbstractSearch(QObject): finished = pyqtSignal(bool) cleared = pyqtSignal() + _Callback = typing.Callable[[bool], None] - def __init__(self, tab, parent=None): + def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) self._tab = tab self._widget = None - self.text = None + self.text = None # type: typing.Optional[str] self.search_displayed = False - def _is_case_sensitive(self, ignore_case): + def _is_case_sensitive(self, ignore_case: str) -> bool: """Check if case-sensitivity should be used. This assumes self.text is already set properly. @@ -273,6 +290,7 @@ class AbstractSearch(QObject): Arguments: ignore_case: The ignore_case value from the config. """ + assert self.text is not None mapping = { 'smart': not self.text.islower(), 'never': True, @@ -280,8 +298,10 @@ class AbstractSearch(QObject): } return mapping[ignore_case] - def search(self, text, *, ignore_case='never', reverse=False, - result_cb=None): + def search(self, text: str, *, + ignore_case: str = 'never', + reverse: bool = False, + result_cb: _Callback = None) -> None: """Find the given text on the page. Args: @@ -292,11 +312,11 @@ class AbstractSearch(QObject): """ raise NotImplementedError - def clear(self): + def clear(self) -> None: """Clear the current search.""" raise NotImplementedError - def prev_result(self, *, result_cb=None): + def prev_result(self, *, result_cb: _Callback = None) -> None: """Go to the previous result of the current search. Args: @@ -304,7 +324,7 @@ class AbstractSearch(QObject): """ raise NotImplementedError - def next_result(self, *, result_cb=None): + def next_result(self, *, result_cb: _Callback = None) -> None: """Go to the next result of the current search. Args: @@ -322,7 +342,7 @@ class AbstractZoom(QObject): _default_zoom_changed: Whether the zoom was changed from the default. """ - def __init__(self, tab, parent=None): + def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) self._tab = tab self._widget = None @@ -339,21 +359,21 @@ class AbstractZoom(QObject): # cfg.changed.disconnect, self.init_neighborlist)) @pyqtSlot(str) - def _on_config_changed(self, option): + def _on_config_changed(self, option: str) -> None: if option in ['zoom.levels', 'zoom.default']: if not self._default_zoom_changed: factor = float(config.val.zoom.default) / 100 self.set_factor(factor) self._init_neighborlist() - def _init_neighborlist(self): + def _init_neighborlist(self) -> None: """Initialize self._neighborlist.""" levels = config.val.zoom.levels self._neighborlist = usertypes.NeighborList( levels, mode=usertypes.NeighborList.Modes.edge) self._neighborlist.fuzzyval = config.val.zoom.default - def offset(self, offset): + def offset(self, offset: int) -> None: """Increase/Decrease the zoom level by the given offset. Args: @@ -366,10 +386,10 @@ class AbstractZoom(QObject): self.set_factor(float(level) / 100, fuzzyval=False) return level - def _set_factor_internal(self, factor): + def _set_factor_internal(self, factor: float) -> None: raise NotImplementedError - def set_factor(self, factor, *, fuzzyval=True): + def set_factor(self, factor: float, *, fuzzyval: bool = True) -> None: """Zoom to a given zoom factor. Args: @@ -387,13 +407,13 @@ class AbstractZoom(QObject): self._zoom_factor = factor self._set_factor_internal(factor) - def factor(self): + def factor(self) -> float: return self._zoom_factor - def set_default(self): + def set_default(self) -> None: self._set_factor_internal(float(config.val.zoom.default) / 100) - def set_current(self): + def set_current(self) -> None: self._set_factor_internal(self._zoom_factor) @@ -410,7 +430,10 @@ class AbstractCaret(QObject): selection_toggled = pyqtSignal(bool) follow_selected_done = pyqtSignal() - def __init__(self, tab, mode_manager, parent=None): + def __init__(self, + tab: 'AbstractTab', + mode_manager: modeman.ModeManager, + parent: QWidget = None) -> None: super().__init__(parent) self._tab = tab self._widget = None @@ -418,74 +441,74 @@ class AbstractCaret(QObject): mode_manager.entered.connect(self._on_mode_entered) mode_manager.left.connect(self._on_mode_left) - def _on_mode_entered(self, mode): + def _on_mode_entered(self, mode: usertypes.KeyMode) -> None: raise NotImplementedError - def _on_mode_left(self, mode): + def _on_mode_left(self, mode: usertypes.KeyMode) -> None: raise NotImplementedError - def move_to_next_line(self, count=1): + def move_to_next_line(self, count: int = 1) -> None: raise NotImplementedError - def move_to_prev_line(self, count=1): + def move_to_prev_line(self, count: int = 1) -> None: raise NotImplementedError - def move_to_next_char(self, count=1): + def move_to_next_char(self, count: int = 1) -> None: raise NotImplementedError - def move_to_prev_char(self, count=1): + def move_to_prev_char(self, count: int = 1) -> None: raise NotImplementedError - def move_to_end_of_word(self, count=1): + def move_to_end_of_word(self, count: int = 1) -> None: raise NotImplementedError - def move_to_next_word(self, count=1): + def move_to_next_word(self, count: int = 1) -> None: raise NotImplementedError - def move_to_prev_word(self, count=1): + def move_to_prev_word(self, count: int = 1) -> None: raise NotImplementedError - def move_to_start_of_line(self): + def move_to_start_of_line(self) -> None: raise NotImplementedError - def move_to_end_of_line(self): + def move_to_end_of_line(self) -> None: raise NotImplementedError - def move_to_start_of_next_block(self, count=1): + def move_to_start_of_next_block(self, count: int = 1) -> None: raise NotImplementedError - def move_to_start_of_prev_block(self, count=1): + def move_to_start_of_prev_block(self, count: int = 1) -> None: raise NotImplementedError - def move_to_end_of_next_block(self, count=1): + def move_to_end_of_next_block(self, count: int = 1) -> None: raise NotImplementedError - def move_to_end_of_prev_block(self, count=1): + def move_to_end_of_prev_block(self, count: int = 1) -> None: raise NotImplementedError - def move_to_start_of_document(self): + def move_to_start_of_document(self) -> None: raise NotImplementedError - def move_to_end_of_document(self): + def move_to_end_of_document(self) -> None: raise NotImplementedError - def toggle_selection(self): + def toggle_selection(self) -> None: raise NotImplementedError - def drop_selection(self): + def drop_selection(self) -> None: raise NotImplementedError - def selection(self, callback): + def selection(self, callback: typing.Callable[[str], None]) -> None: raise NotImplementedError - def _follow_enter(self, tab): + def _follow_enter(self, tab: bool) -> None: """Follow a link by faking an enter press.""" if tab: self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) else: self._tab.key_press(Qt.Key_Enter) - def follow_selected(self, *, tab=False): + def follow_selected(self, *, tab: bool = False) -> None: raise NotImplementedError @@ -495,69 +518,69 @@ class AbstractScroller(QObject): perc_changed = pyqtSignal(int, int) - def __init__(self, tab, parent=None): + def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) self._tab = tab - self._widget = None + self._widget = None # type: typing.Optional[QWebEngineView] self.perc_changed.connect(self._log_scroll_pos_change) @pyqtSlot() - def _log_scroll_pos_change(self): - log.webview.vdebug("Scroll position changed to {}".format( - self.pos_px())) + def _log_scroll_pos_change(self) -> None: + log.webview.vdebug( # type: ignore + "Scroll position changed to {}".format(self.pos_px())) - def _init_widget(self, widget): + def _init_widget(self, widget: 'QWebEngineView') -> None: self._widget = widget - def pos_px(self): + def pos_px(self) -> int: raise NotImplementedError - def pos_perc(self): + def pos_perc(self) -> int: raise NotImplementedError - def to_perc(self, x=None, y=None): + def to_perc(self, x: int = None, y: int = None) -> None: raise NotImplementedError - def to_point(self, point): + def to_point(self, point: QPoint) -> None: raise NotImplementedError - def to_anchor(self, name): + def to_anchor(self, name: str) -> None: raise NotImplementedError - def delta(self, x=0, y=0): + def delta(self, x: int = 0, y: int = 0) -> None: raise NotImplementedError - def delta_page(self, x=0, y=0): + def delta_page(self, x: float = 0, y: float = 0) -> None: raise NotImplementedError - def up(self, count=1): + def up(self, count: int = 1) -> None: raise NotImplementedError - def down(self, count=1): + def down(self, count: int = 1) -> None: raise NotImplementedError - def left(self, count=1): + def left(self, count: int = 1) -> None: raise NotImplementedError - def right(self, count=1): + def right(self, count: int = 1) -> None: raise NotImplementedError - def top(self): + def top(self) -> None: raise NotImplementedError - def bottom(self): + def bottom(self) -> None: raise NotImplementedError - def page_up(self, count=1): + def page_up(self, count: int = 1) -> None: raise NotImplementedError - def page_down(self, count=1): + def page_down(self, count: int = 1) -> None: raise NotImplementedError - def at_top(self): + def at_top(self) -> bool: raise NotImplementedError - def at_bottom(self): + def at_bottom(self) -> bool: raise NotImplementedError @@ -565,20 +588,20 @@ class AbstractHistory: """The history attribute of a AbstractTab.""" - def __init__(self, tab): + def __init__(self, tab: 'AbstractTab') -> None: self._tab = tab self._history = None - def __len__(self): - return len(self._history) - - def __iter__(self): - return iter(self._history.items()) - - def current_idx(self): + def __len__(self) -> int: raise NotImplementedError - def back(self, count=1): + def __iter__(self) -> typing.Iterable: + raise NotImplementedError + + def current_idx(self) -> int: + raise NotImplementedError + + def back(self, count: int = 1) -> None: """Go back in the tab's history.""" idx = self.current_idx() - count if idx >= 0: @@ -587,7 +610,7 @@ class AbstractHistory: self._go_to_item(self._item_at(0)) raise WebTabError("At beginning of history.") - def forward(self, count=1): + def forward(self, count: int = 1) -> None: """Go forward in the tab's history.""" idx = self.current_idx() + count if idx < len(self): @@ -596,27 +619,27 @@ class AbstractHistory: self._go_to_item(self._item_at(len(self) - 1)) raise WebTabError("At end of history.") - def can_go_back(self): + def can_go_back(self) -> bool: raise NotImplementedError - def can_go_forward(self): + def can_go_forward(self) -> bool: raise NotImplementedError - def _item_at(self, i): + def _item_at(self, i: int) -> typing.Any: raise NotImplementedError - def _go_to_item(self, item): + def _go_to_item(self, item: typing.Any) -> None: raise NotImplementedError - def serialize(self): + def serialize(self) -> bytes: """Serialize into an opaque format understood by self.deserialize.""" raise NotImplementedError - def deserialize(self, data): - """Serialize from a format produced by self.serialize.""" + def deserialize(self, data: bytes) -> None: + """Deserialize from a format produced by self.serialize.""" raise NotImplementedError - def load_items(self, items): + def load_items(self, items: typing.Sequence) -> None: """Deserialize from a list of WebHistoryItems.""" raise NotImplementedError @@ -625,11 +648,18 @@ class AbstractElements: """Finding and handling of elements on the page.""" - def __init__(self, tab): + _MultiCallback = typing.Callable[ + [typing.Sequence[webelem.AbstractWebElement]], None] + _SingleCallback = typing.Callable[ + [typing.Optional[webelem.AbstractWebElement]], None] + + def __init__(self, tab: 'AbstractTab') -> None: self._widget = None self._tab = tab - def find_css(self, selector, callback, *, only_visible=False): + def find_css(self, selector: str, + callback: _MultiCallback, *, + only_visible: bool = False) -> None: """Find all HTML elements matching a given selector async. If there's an error, the callback is called with a webelem.Error @@ -642,7 +672,7 @@ class AbstractElements: """ raise NotImplementedError - def find_id(self, elem_id, callback): + def find_id(self, elem_id: str, callback: _SingleCallback) -> None: """Find the HTML element with the given ID async. Args: @@ -651,7 +681,7 @@ class AbstractElements: """ raise NotImplementedError - def find_focused(self, callback): + def find_focused(self, callback: _SingleCallback) -> None: """Find the focused element on the page async. Args: @@ -660,7 +690,7 @@ class AbstractElements: """ raise NotImplementedError - def find_at_pos(self, pos, callback): + def find_at_pos(self, pos: QPoint, callback: _SingleCallback) -> None: """Find the element at the given position async. This is also called "hit test" elsewhere. @@ -680,9 +710,9 @@ class AbstractAudio(QObject): muted_changed = pyqtSignal(bool) recently_audible_changed = pyqtSignal(bool) - def __init__(self, tab, parent=None): + def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) - self._widget = None + self._widget = None # type: typing.Optional[QWebEngineView] self._tab = tab def set_muted(self, muted: bool, override: bool = False) -> None: @@ -695,14 +725,14 @@ class AbstractAudio(QObject): """ raise NotImplementedError - def is_muted(self): + def is_muted(self) -> bool: """Whether this tab is muted.""" raise NotImplementedError def toggle_muted(self, *, override: bool = False) -> None: self.set_muted(not self.is_muted(), override=override) - def is_recently_audible(self): + def is_recently_audible(self) -> bool: """Whether this tab has had audio playing recently.""" raise NotImplementedError @@ -758,7 +788,11 @@ class AbstractTab(QWidget): renderer_process_terminated = pyqtSignal(TerminationStatus, int) predicted_navigation = pyqtSignal(QUrl) - def __init__(self, *, win_id, mode_manager, private, parent=None): + def __init__(self, *, + win_id: int, + mode_manager: modeman.ModeManager, + private: bool, + parent: QWidget = None) -> None: self.private = private self.win_id = win_id self.tab_id = next(tab_id_gen) @@ -772,7 +806,7 @@ class AbstractTab(QWidget): self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) - self._widget = None + self._widget = None # type: typing.Optional[QWebEngineView] self._progress = 0 self._has_ssl_errors = False self._mode_manager = mode_manager @@ -789,7 +823,7 @@ class AbstractTab(QWidget): self.predicted_navigation.connect(self._on_predicted_navigation) - def _set_widget(self, widget): + def _set_widget(self, widget: 'QWebEngineView') -> None: # pylint: disable=protected-access self._widget = widget self._layout.wrap(self, widget) @@ -807,10 +841,10 @@ class AbstractTab(QWidget): self._install_event_filter() self.zoom.set_default() - def _install_event_filter(self): + def _install_event_filter(self) -> None: raise NotImplementedError - def _set_load_status(self, val): + def _set_load_status(self, val: usertypes.LoadStatus) -> None: """Setter for load_status.""" if not isinstance(val, usertypes.LoadStatus): raise TypeError("Type {} is no LoadStatus member!".format(val)) @@ -818,11 +852,11 @@ class AbstractTab(QWidget): self._load_status = val self.load_status_changed.emit(val.name) - def event_target(self): + def event_target(self) -> QWidget: """Return the widget events should be sent to.""" raise NotImplementedError - def send_event(self, evt): + def send_event(self, evt: QEvent) -> None: """Send the given event to the underlying widget. The event will be sent via QApplication.postEvent. @@ -844,7 +878,7 @@ class AbstractTab(QWidget): QApplication.postEvent(recipient, evt) @pyqtSlot(QUrl) - def _on_predicted_navigation(self, url): + def _on_predicted_navigation(self, url: QUrl) -> None: """Adjust the title if we are going to visit a URL soon.""" qtutils.ensure_valid(url) url_string = url.toDisplayString() @@ -852,14 +886,14 @@ class AbstractTab(QWidget): self.title_changed.emit(url_string) @pyqtSlot(QUrl) - def _on_url_changed(self, url): + def _on_url_changed(self, url: QUrl) -> None: """Update title when URL has changed and no title is available.""" if url.isValid() and not self.title(): self.title_changed.emit(url.toDisplayString()) self.url_changed.emit(url) @pyqtSlot() - def _on_load_started(self): + def _on_load_started(self) -> None: self._progress = 0 self._has_ssl_errors = False self.data.viewing_source = False @@ -867,7 +901,10 @@ class AbstractTab(QWidget): self.load_started.emit() @pyqtSlot(usertypes.NavigationRequest) - def _on_navigation_request(self, navigation): + def _on_navigation_request( + self, + navigation: usertypes.NavigationRequest + ) -> None: """Handle common acceptNavigationRequest code.""" url = utils.elide(navigation.url.toDisplayString(), 100) log.webview.debug("navigation request: url {}, type {}, is_main_frame " @@ -891,7 +928,7 @@ class AbstractTab(QWidget): navigation.url.errorString())) navigation.accepted = False - def handle_auto_insert_mode(self, ok): + def handle_auto_insert_mode(self, ok: bool) -> None: """Handle `input.insert_mode.auto_load` after loading finished.""" if not config.val.input.insert_mode.auto_load or not ok: return @@ -900,7 +937,7 @@ class AbstractTab(QWidget): if cur_mode == usertypes.KeyMode.insert: return - def _auto_insert_mode_cb(elem): + def _auto_insert_mode_cb(elem: webelem.AbstractWebElement) -> None: """Called from JS after finding the focused element.""" if elem is None: log.webview.debug("No focused element!") @@ -912,7 +949,8 @@ class AbstractTab(QWidget): self.elements.find_focused(_auto_insert_mode_cb) @pyqtSlot(bool) - def _on_load_finished(self, ok): + def _on_load_finished(self, ok: bool) -> None: + assert self._widget is not None if sip.isdeleted(self._widget): # https://github.com/qutebrowser/qutebrowser/issues/3498 return @@ -943,46 +981,50 @@ class AbstractTab(QWidget): self.zoom.set_current() @pyqtSlot() - def _on_history_trigger(self): + def _on_history_trigger(self) -> None: """Emit add_history_item when triggered by backend-specific signal.""" raise NotImplementedError @pyqtSlot(int) - def _on_load_progress(self, perc): + def _on_load_progress(self, perc: int) -> None: self._progress = perc self.load_progress.emit(perc) - def url(self, requested=False): + def url(self, requested: bool = False) -> QUrl: raise NotImplementedError - def progress(self): + def progress(self) -> int: return self._progress - def load_status(self): + def load_status(self) -> usertypes.LoadStatus: return self._load_status - def _openurl_prepare(self, url, *, predict=True): + def _openurl_prepare(self, url: QUrl, *, predict: bool = True) -> None: qtutils.ensure_valid(url) if predict: self.predicted_navigation.emit(url) - def openurl(self, url, *, predict=True): + def openurl(self, url: QUrl, *, predict: bool = True) -> None: raise NotImplementedError - def reload(self, *, force=False): + def reload(self, *, force: bool = False) -> None: raise NotImplementedError - def stop(self): + def stop(self) -> None: raise NotImplementedError - def clear_ssl_errors(self): + def clear_ssl_errors(self) -> None: raise NotImplementedError - def key_press(self, key, modifier=Qt.NoModifier): + def key_press(self, + key: Qt.Key, + modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: """Send a fake key event to this tab.""" raise NotImplementedError - def dump_async(self, callback, *, plain=False): + def dump_async(self, + callback: typing.Callable[[str], None], *, + plain: bool = False) -> None: """Dump the current page's html asynchronously. The given callback will be called with the result when dumping is @@ -990,7 +1032,12 @@ class AbstractTab(QWidget): """ raise NotImplementedError - def run_js_async(self, code, callback=None, *, world=None): + def run_js_async( + self, + code: str, + callback: typing.Callable[[typing.Any], None] = None, *, + world: typing.Union[usertypes.JsWorld, int] = None + ) -> None: """Run javascript async. The given callback will be called with the result when running JS is @@ -1004,19 +1051,19 @@ class AbstractTab(QWidget): """ raise NotImplementedError - def shutdown(self): + def shutdown(self) -> None: raise NotImplementedError - def title(self): + def title(self) -> str: raise NotImplementedError - def icon(self): + def icon(self) -> None: raise NotImplementedError - def set_html(self, html, base_url=QUrl()): + def set_html(self, html: str, base_url: QUrl = QUrl()) -> None: raise NotImplementedError - def networkaccessmanager(self): + def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]: """Get the QNetworkAccessManager for this tab. This is only implemented for QtWebKit. @@ -1024,7 +1071,7 @@ class AbstractTab(QWidget): """ raise NotImplementedError - def user_agent(self): + def user_agent(self) -> typing.Optional[str]: """Get the user agent for this tab. This is only implemented for QtWebKit. @@ -1032,13 +1079,16 @@ class AbstractTab(QWidget): """ raise NotImplementedError - def __repr__(self): + def __repr__(self) -> str: try: - url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), - 100) + qurl = self.url() + url = qurl.toDisplayString(QUrl.EncodeUnicode) # type: ignore except (AttributeError, RuntimeError) as exc: url = '<{}>'.format(exc.__class__.__name__) + else: + url = utils.elide(url, 100) return utils.get_repr(self, tab_id=self.tab_id, url=url) - def is_deleted(self): + def is_deleted(self) -> bool: + assert self._widget is not None return sip.isdeleted(self._widget) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 9945886fa..47bacd60e 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -522,6 +522,12 @@ class WebEngineHistory(browsertab.AbstractHistory): """QtWebEngine implementations related to page history.""" + def __len__(self): + return len(self._history) + + def __iter__(self): + return iter(self._history.items()) + def current_idx(self): return self._history.currentItemIndex() @@ -551,7 +557,7 @@ class WebEngineHistory(browsertab.AbstractHistory): return qtutils.serialize(self._history) def deserialize(self, data): - return qtutils.deserialize(data, self._history) + qtutils.deserialize(data, self._history) def load_items(self, items): if items: @@ -672,6 +678,7 @@ class WebEngineAudio(browsertab.AbstractAudio): def set_muted(self, muted: bool, override: bool = False) -> None: self._overridden = override + assert self._widget is not None page = self._widget.page() page.setAudioMuted(muted) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index c791326ce..51e3f385e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -513,6 +513,12 @@ class WebKitHistory(browsertab.AbstractHistory): """QtWebKit implementations related to page history.""" + def __len__(self): + return len(self._history) + + def __iter__(self): + return iter(self._history.items()) + def current_idx(self): return self._history.currentItemIndex() @@ -533,7 +539,7 @@ class WebKitHistory(browsertab.AbstractHistory): return qtutils.serialize(self._history) def deserialize(self, data): - return qtutils.deserialize(data, self._history) + qtutils.deserialize(data, self._history) def load_items(self, items): if items: From 73be27ac13cee765bf521af4f937499db75888e6 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Thu, 22 Nov 2018 08:06:04 -0800 Subject: [PATCH 068/258] Add org-capture by alcah to userscripts README --- misc/userscripts/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md index 5680267b7..ceddb6c81 100644 --- a/misc/userscripts/README.md +++ b/misc/userscripts/README.md @@ -53,9 +53,10 @@ The following userscripts can be found on their own repositories. - [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts. - [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your [Pinboard][] bookmark manager. +- [qute-capture](https://github.com/alcah/qute-capture): Capture links with + Emacs's org-mode to a read-later file. [Zotero]: https://www.zotero.org/ [Pocket]: https://getpocket.com/ [Instapaper]: https://www.instapaper.com/ [Pinboard]: https://pinboard.in/ - From bc93d2257f25402275d945e7fc7e3ac0b0159dd1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 09:20:48 +0100 Subject: [PATCH 069/258] Add WM_CLASS to the FAQ --- doc/faq.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 6687917c7..113a11f09 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -260,6 +260,12 @@ Note that there are some missing features which you may run into: . 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.:: From 52c703a5ff45f4851c8fb13569a86151d7895bcf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 11:48:46 +0100 Subject: [PATCH 070/258] Update pylint to 2.2.2 --- 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 44c2b19e8..8329ae0dd 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -13,7 +13,7 @@ jwcrypto==0.6.0 lazy-object-proxy==1.3.1 mccabe==0.6.1 pycparser==2.19 -pylint==2.2.0 +pylint==2.2.2 python-dateutil==2.7.5 ./scripts/dev/pylint_checkers requests==2.20.1 From 5ed3fb723d52092a1609c097c8a16e709a95911f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 11:49:10 +0100 Subject: [PATCH 071/258] Regenerate requirements-tests --- misc/requirements/requirements-tests.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 6ea860f19..7d8a4f33e 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -6,10 +6,9 @@ backports.functools-lru-cache==1.5 beautifulsoup4==4.6.3 cheroot==6.5.2 Click==7.0 -# colorama==0.4.0 +# colorama==0.4.1 coverage==4.5.2 EasyProcess==0.2.3 -fields==5.0.0 Flask==1.0.2 glob2==0.6 hunter==2.1.0 From 2453134011a8a7fc3e813d55e578b72ea07a5a42 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 11:26:40 +0100 Subject: [PATCH 072/258] Remove unnecessary pass statements --- qutebrowser/browser/downloads.py | 2 -- qutebrowser/browser/inspector.py | 2 -- qutebrowser/browser/network/pac.py | 4 --- qutebrowser/browser/qutescheme.py | 10 ------- qutebrowser/browser/urlmarks.py | 8 ------ qutebrowser/browser/webelem.py | 4 --- qutebrowser/browser/webkit/mhtml.py | 1 - .../browser/webkit/network/networkreply.py | 3 --- qutebrowser/browser/webkit/webkitelem.py | 2 -- qutebrowser/browser/webkit/webkithistory.py | 1 - qutebrowser/commands/cmdexc.py | 8 ------ qutebrowser/config/configexc.py | 2 -- qutebrowser/mainwindow/statusbar/keystring.py | 2 -- qutebrowser/misc/cmdhistory.py | 4 --- qutebrowser/misc/crashdialog.py | 2 -- qutebrowser/misc/sql.py | 4 --- qutebrowser/misc/utilcmds.py | 1 - qutebrowser/utils/objreg.py | 2 -- qutebrowser/utils/utils.py | 1 - scripts/hist_importer.py | 2 -- scripts/link_pyqt.py | 2 -- tests/end2end/fixtures/quteprocess.py | 2 -- tests/end2end/fixtures/testprocess.py | 6 ----- tests/helpers/stubs.py | 5 ---- tests/unit/commands/test_cmdutils.py | 26 +------------------ tests/unit/completion/test_completer.py | 6 ----- tests/unit/utils/test_qtutils.py | 5 ---- tests/unit/utils/test_utils.py | 9 ------- tests/unit/utils/usertypes/test_timer.py | 2 -- 29 files changed, 1 insertion(+), 127 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 92d846bd8..6d9e4b388 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -60,8 +60,6 @@ class UnsupportedAttribute: supported with QtWebengine. """ - pass - class UnsupportedOperationError(Exception): diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index 9b3fced2b..3334cea4e 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -49,8 +49,6 @@ class WebInspectorError(Exception): """Raised when the inspector could not be initialized.""" - pass - class AbstractWebInspector(QWidget): diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 95ff99390..1c6075945 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -35,15 +35,11 @@ class ParseProxyError(Exception): """Error while parsing PAC result string.""" - pass - class EvalProxyError(Exception): """Error while evaluating PAC script.""" - pass - def _js_slot(*args): """Wrap a methods as a JavaScript function. diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 0fa9366a6..769247e88 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -61,36 +61,26 @@ class Error(Exception): """Exception for generic errors on a qute:// page.""" - pass - class NotFoundError(Error): """Raised when the given URL was not found.""" - pass - class SchemeOSError(Error): """Raised when there was an OSError inside a handler.""" - pass - class UrlInvalidError(Error): """Raised when an invalid URL was opened.""" - pass - class RequestDeniedError(Error): """Raised when the request is forbidden.""" - pass - class Redirect(Exception): diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index bb20e2166..e32dbcc46 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -43,29 +43,21 @@ class Error(Exception): """Base class for all errors in this module.""" - pass - class InvalidUrlError(Error): """Exception emitted when a URL is invalid.""" - pass - class DoesNotExistError(Error): """Exception emitted when a given URL does not exist.""" - pass - class AlreadyExistsError(Error): """Exception emitted when a given URL does already exist.""" - pass - class UrlMarkManager(QObject): diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index e39eb5850..520b7d12b 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -34,15 +34,11 @@ class Error(Exception): """Base class for WebElement errors.""" - pass - class OrphanedError(Error): """Raised when a webelement's parent has vanished.""" - pass - def css_selector(group, url): """Get a CSS selector for the given group/URL.""" diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 9339df9ce..1ecebed2d 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -516,7 +516,6 @@ class _NoCloseBytesIO(io.BytesIO): def close(self): """Do nothing.""" - pass def actual_close(self): """Close the stream.""" diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index dc6bed5ed..c56fe2a9b 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -67,7 +67,6 @@ class FixedDataNetworkReply(QNetworkReply): @pyqtSlot() def abort(self): """Abort the operation.""" - pass def bytesAvailable(self): """Determine the bytes available for being read. @@ -123,7 +122,6 @@ class ErrorNetworkReply(QNetworkReply): def abort(self): """Do nothing since it's a fake reply.""" - pass def bytesAvailable(self): """We always have 0 bytes available.""" @@ -151,7 +149,6 @@ class RedirectNetworkReply(QNetworkReply): def abort(self): """Called when there's e.g. a redirection limit.""" - pass def readData(self, _maxlen): return bytes() diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 01a2736dc..66d5e59b8 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -31,8 +31,6 @@ class IsNullError(webelem.Error): """Gets raised by WebKitElement if an element is null.""" - pass - class WebKitElement(webelem.AbstractWebElement): diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index 2c719acd6..65d9bbb01 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -41,7 +41,6 @@ class WebHistoryInterface(QWebHistoryInterface): def addHistoryEntry(self, url_string): """Required for a QWebHistoryInterface impl, obsoleted by add_url.""" - pass @functools.lru_cache(maxsize=32768) def historyContains(self, url_string): diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index 5d3ac2a89..dbc5da581 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -32,22 +32,16 @@ class CommandError(Error): """Raised when a command encounters an error while running.""" - pass - class NoSuchCommandError(Error): """Raised when a command wasn't found.""" - pass - class ArgumentTypeError(Error): """Raised when an argument had an invalid type.""" - pass - class PrerequisitesError(Error): @@ -56,5 +50,3 @@ class PrerequisitesError(Error): This is raised for example when we're in the wrong mode while executing the command, or we need javascript enabled but don't have done so. """ - - pass diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index 2a99dfa5c..051ed971a 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -28,8 +28,6 @@ class Error(Exception): """Base exception for config-related errors.""" - pass - class NoAutoconfigError(Error): diff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py index 29dd3b790..73b23a65d 100644 --- a/qutebrowser/mainwindow/statusbar/keystring.py +++ b/qutebrowser/mainwindow/statusbar/keystring.py @@ -25,5 +25,3 @@ from qutebrowser.mainwindow.statusbar import textbase class KeyString(textbase.TextBase): """Keychain string displayed in the statusbar.""" - - pass diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index 9fa273c1c..0a3ac9fa9 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -29,15 +29,11 @@ class HistoryEmptyError(Exception): """Raised when the history is empty.""" - pass - class HistoryEndReachedError(Exception): """Raised when the end of the history is reached.""" - pass - class History(QObject): diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 27dec3345..e2b9bd05f 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -197,7 +197,6 @@ class _CrashDialog(QDialog): def _init_checkboxes(self): """Initialize the checkboxes.""" - pass def _init_buttons(self): """Initialize the buttons.""" @@ -569,7 +568,6 @@ class ReportDialog(_CrashDialog): def _init_info_text(self): """We don't want an info text as the user wanted to report.""" - pass def _get_error_type(self): return 'report' diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 4c300a3da..eda778194 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -72,8 +72,6 @@ class SqlEnvironmentError(SqlError): disk or I/O errors), where qutebrowser isn't to blame. """ - pass - class SqlBugError(SqlError): @@ -82,8 +80,6 @@ class SqlBugError(SqlError): This is raised for errors resulting from a qutebrowser bug. """ - pass - def raise_sqlite_error(msg, error): """Raise either a SqlBugError or SqlEnvironmentError.""" diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index d108a56ac..1245d06ba 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -370,7 +370,6 @@ def window_only(current_win_id): @cmdutils.register() def nop(): """Do nothing.""" - pass @cmdutils.register() diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 17fc34b92..b68ca133c 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -42,8 +42,6 @@ class RegistryUnavailableError(Exception): """Exception raised when a certain registry does not exist yet.""" - pass - class NoWindow(Exception): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 6119675ba..1ac7a1a8c 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -630,7 +630,6 @@ def open_file(filename, cmdline=None): def unused(_arg): """Function which does nothing to avoid pylint complaining.""" - pass def expand_windows_drive(path): diff --git a/scripts/hist_importer.py b/scripts/hist_importer.py index 914701a19..31936b4c1 100755 --- a/scripts/hist_importer.py +++ b/scripts/hist_importer.py @@ -33,8 +33,6 @@ class Error(Exception): """Exception for errors in this module.""" - pass - def parse(): """Parse command line arguments.""" diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index ae7eaa622..e16056fa8 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -34,8 +34,6 @@ class Error(Exception): """Exception raised when linking fails.""" - pass - def run_py(executable, *code): """Run the given python code with the given executable.""" diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index f4e5d1486..561d92a80 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -842,8 +842,6 @@ class YamlLoader(yaml.SafeLoader): """Custom YAML loader used in compare_session.""" - pass - # Translate ... to ellipsis in YAML. YamlLoader.add_constructor('!ellipsis', lambda loader, node: ...) diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index d0af5bebc..eab914a1a 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -39,15 +39,11 @@ class InvalidLine(Exception): """Raised when the process prints a line which is not parsable.""" - pass - class ProcessExited(Exception): """Raised when the child process did exit.""" - pass - class WaitForTimeout(Exception): @@ -271,7 +267,6 @@ class Process(QObject): def _after_start(self): """Do things which should be done immediately after starting.""" - pass def before_test(self): """Restart process before a test if it exited before.""" @@ -443,7 +438,6 @@ class Process(QObject): QuteProc._maybe_skip, and call _maybe_skip after every parsed message in wait_for (where it's most likely that new messages arrive). """ - pass def wait_for(self, timeout=None, *, override_waited_for=False, do_skip=False, divisor=1, after=None, **kwargs): diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 9bf5b837d..e050e1fb5 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -306,7 +306,6 @@ class FakeSignal: Currently does nothing, but could be improved to do some sanity checking on the slot. """ - pass def disconnect(self, slot=None): """Disconnect the signal from a slot. @@ -314,7 +313,6 @@ class FakeSignal: Currently does nothing, but could be improved to do some sanity checking on the slot and see if it actually got connected. """ - pass def emit(self, *args): """Emit the signal. @@ -322,7 +320,6 @@ class FakeSignal: Currently does nothing, but could be improved to do type checking based on a signature given to __init__. """ - pass @attr.s @@ -457,8 +454,6 @@ class BookmarkManagerStub(UrlMarkManagerStub): """Stub for the bookmark-manager object.""" - pass - class QuickmarkManagerStub(UrlMarkManagerStub): diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index fc68fa920..ef7c97164 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -48,7 +48,6 @@ def _get_cmd(*args, **kwargs): @cmdutils.register(*args, **kwargs) def fun(): """Blah.""" - pass return cmdutils.cmd_dict['fun'] @@ -83,7 +82,6 @@ class TestRegister: @cmdutils.register() def fun(): """Blah.""" - pass cmd = cmdutils.cmd_dict['fun'] assert cmd.handler is fun @@ -95,7 +93,6 @@ class TestRegister: @cmdutils.register() def eggs_bacon(): """Blah.""" - pass assert cmdutils.cmd_dict['eggs-bacon'].name == 'eggs-bacon' assert 'eggs_bacon' not in cmdutils.cmd_dict @@ -105,7 +102,6 @@ class TestRegister: @cmdutils.register() def Test(): # noqa: N801,N806 pylint: disable=invalid-name """Blah.""" - pass assert cmdutils.cmd_dict['test'].name == 'test' assert 'Test' not in cmdutils.cmd_dict @@ -115,7 +111,6 @@ class TestRegister: @cmdutils.register(name='foobar') def fun(): """Blah.""" - pass assert cmdutils.cmd_dict['foobar'].name == 'foobar' assert 'fun' not in cmdutils.cmd_dict @@ -126,20 +121,17 @@ class TestRegister: @cmdutils.register(name='foobar') def fun(): """Blah.""" - pass with pytest.raises(ValueError): @cmdutils.register(name='foobar') def fun2(): """Blah.""" - pass def test_instance(self): """Make sure the instance gets passed to Command.""" @cmdutils.register(instance='foobar') def fun(self): """Blah.""" - pass assert cmdutils.cmd_dict['fun']._instance == 'foobar' def test_star_args(self): @@ -147,7 +139,6 @@ class TestRegister: @cmdutils.register() def fun(*args): """Blah.""" - pass with pytest.raises(argparser.ArgumentParserError): cmdutils.cmd_dict['fun'].parser.parse_args([]) @@ -195,14 +186,12 @@ class TestRegister: @cmdutils.argument('arg1', flag='b') def fun(arg1=False, arg2=False): """Blah.""" - pass def test_win_id(self): @cmdutils.register() @cmdutils.argument('win_id', win_id=True) def fun(win_id): """Blah.""" - pass assert cmdutils.cmd_dict['fun']._get_call_args(42) == ([42], {}) def test_count(self): @@ -210,7 +199,6 @@ class TestRegister: @cmdutils.argument('count', count=True) def fun(count=0): """Blah.""" - pass assert cmdutils.cmd_dict['fun']._get_call_args(42) == ([0], {}) def test_count_without_default(self): @@ -220,7 +208,6 @@ class TestRegister: @cmdutils.argument('count', count=True) def fun(count): """Blah.""" - pass @pytest.mark.parametrize('hide', [True, False]) def test_pos_args(self, hide): @@ -228,7 +215,6 @@ class TestRegister: @cmdutils.argument('arg', hide=hide) def fun(arg): """Blah.""" - pass pos_args = cmdutils.cmd_dict['fun'].pos_args if hide: @@ -283,7 +269,6 @@ class TestRegister: @cmdutils.argument('arg', choices=['foo', 'bar']) def fun(arg): """Blah.""" - pass cmd = cmdutils.cmd_dict['fun'] cmd.namespace = cmd.parser.parse_args(['fish']) @@ -297,7 +282,6 @@ class TestRegister: @cmdutils.argument('arg', choices=['foo', 'bar']) def fun(*, arg='foo'): """Blah.""" - pass cmd = cmdutils.cmd_dict['fun'] cmd.namespace = cmd.parser.parse_args(['--arg=fish']) @@ -312,7 +296,6 @@ class TestRegister: @cmdutils.argument('opt') def fun(foo, bar, opt=False): """Blah.""" - pass cmd = cmdutils.cmd_dict['fun'] assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b')) @@ -324,7 +307,6 @@ class TestRegister: # https://github.com/qutebrowser/qutebrowser/issues/1872 def fun(*, target): """Blah.""" - pass with pytest.raises(TypeError, match="fun: handler has keyword only " "argument 'target' without default!"): @@ -334,7 +316,6 @@ class TestRegister: # https://github.com/qutebrowser/qutebrowser/issues/1872 def fun(*, target: int): """Blah.""" - pass with pytest.raises(TypeError, match="fun: handler has keyword only " "argument 'target' without default!"): @@ -350,14 +331,12 @@ class TestArgument: @cmdutils.argument('foo') def fun(bar): """Blah.""" - pass def test_storage(self): @cmdutils.argument('foo', flag='x') @cmdutils.argument('bar', flag='y') def fun(foo, bar): """Blah.""" - pass expected = { 'foo': command.ArgInfo(flag='x'), 'bar': command.ArgInfo(flag='y') @@ -372,7 +351,6 @@ class TestArgument: @cmdutils.register() def fun(bar): """Blah.""" - pass def test_count_and_win_id_same_arg(self): with pytest.raises(TypeError, @@ -380,7 +358,6 @@ class TestArgument: @cmdutils.argument('arg', count=True, win_id=True) def fun(arg=0): """Blah.""" - pass def test_no_docstring(self, caplog): with caplog.at_level(logging.WARNING): @@ -388,6 +365,7 @@ class TestArgument: def fun(): # no docstring pass + assert len(caplog.records) == 1 assert caplog.messages[0].endswith('test_cmdutils.py has no docstring') @@ -398,7 +376,6 @@ class TestArgument: @cmdutils.register() def fun(): # no docstring - pass class TestRun: @@ -441,7 +418,6 @@ class TestRun: backend=usertypes.Backend.QtWebEngine) def fun(self): """Blah.""" - pass monkeypatch.setattr(command.objects, 'backend', usertypes.Backend.QtWebKit) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index dc98f0efb..8620241e4 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -105,35 +105,29 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch): @cmdutils.argument('value', completion=miscmodels_patch.value) def set_command(section_=None, option=None, value=None): """docstring.""" - pass @cmdutils.argument('topic', completion=miscmodels_patch.helptopic) def show_help(tab=False, bg=False, window=False, topic=None): """docstring.""" - pass @cmdutils.argument('url', completion=miscmodels_patch.url) @cmdutils.argument('count', count=True) def openurl(url=None, related=False, bg=False, tab=False, window=False, count=None): """docstring.""" - pass @cmdutils.argument('win_id', win_id=True) @cmdutils.argument('command', completion=miscmodels_patch.command) def bind(key, win_id, command=None, *, mode='normal'): """docstring.""" - pass def tab_give(): """docstring.""" - pass @cmdutils.argument('option', completion=miscmodels_patch.option) @cmdutils.argument('values', completion=miscmodels_patch.value) def config_cycle(option, *values): """For testing varargs.""" - pass cmd_utils = stubs.FakeCmdUtils({ 'set': command.Command(name='set', handler=set_command), diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 0825d1fb3..3b625b3a5 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -336,8 +336,6 @@ class SavefileTestException(Exception): """Exception raised in TestSavefileOpen for testing.""" - pass - @pytest.mark.usefixtures('qapp') class TestSavefileOpen: @@ -541,7 +539,6 @@ if test_file is not None: def testReadinto_text(self): """Skip this test as BufferedIOBase seems to fail it.""" - pass class PyOtherFileTests(PyIODeviceTestMixin, test_file.OtherFileTests, unittest.TestCase): @@ -550,11 +547,9 @@ if test_file is not None: def testSetBufferSize(self): """Skip this test as setting buffer size is unsupported.""" - pass def testTruncateOnWindows(self): """Skip this test truncating is unsupported.""" - pass class FailingQIODevice(QIODevice): diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index e25ecfe4a..ccabfa050 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -409,8 +409,6 @@ class GotException(Exception): """Exception used for TestDisabledExcepthook.""" - pass - def excepthook(_exc, _val, _tb): pass @@ -512,8 +510,6 @@ class Obj: """Test object for test_get_repr().""" - pass - @pytest.mark.parametrize('constructor, attrs, expected', [ (False, {}, ''), @@ -534,12 +530,10 @@ class QualnameObj(): def func(self): """Test method for test_qualname.""" - pass def qualname_func(_blah): """Test function for test_qualname.""" - pass QUALNAME_OBJ = QualnameObj() @@ -578,8 +572,6 @@ class TestIsEnum: """Test class for is_enum.""" - pass - assert not utils.is_enum(Test) def test_object(self): @@ -597,7 +589,6 @@ class TestRaises: def do_nothing(self): """Helper function which does nothing.""" - pass @pytest.mark.parametrize('exception, value, expected', [ (ValueError, 'a', True), diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index 2ead0dc60..928e9d6a8 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -29,8 +29,6 @@ class Parent(QObject): """Class for test_parent().""" - pass - def test_parent(): """Make sure the parent is set correctly.""" From 2237ca2bcfa94d5521d735c66c3c4b83759ed763 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 12:54:19 +0100 Subject: [PATCH 073/258] Remove now unneeded pylint suppressions --- qutebrowser/browser/webengine/webenginedownloads.py | 3 +-- qutebrowser/misc/backendproblem.py | 1 - qutebrowser/misc/earlyinit.py | 3 +-- qutebrowser/utils/qtutils.py | 2 +- scripts/dev/build_release.py | 2 +- tests/conftest.py | 3 +-- tests/unit/misc/test_checkpyver.py | 2 +- tests/unit/utils/test_utils.py | 4 ---- 8 files changed, 6 insertions(+), 14 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 94caa887e..6dde42070 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -117,8 +117,7 @@ class DownloadItem(downloads.AbstractDownloadItem): def _get_open_filename(self): return self._filename - def _set_fileobj(self, fileobj, *, - autoclose=True): # pylint: disable=unused-argument + def _set_fileobj(self, fileobj, *, autoclose=True): raise downloads.UnsupportedOperationError def _set_tempfile(self, fileobj): diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index d5f7c9680..53094a257 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -280,7 +280,6 @@ class BackendImports: def _try_import_backends(): """Check whether backends can be imported and return BackendImports.""" - # pylint: disable=unused-variable results = BackendImports() try: diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index b29e1508f..5e650510c 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -133,7 +133,7 @@ def init_faulthandler(fileobj=sys.__stderr__): def check_pyqt_core(): """Check if PyQt core is installed.""" try: - import PyQt5.QtCore # pylint: disable=unused-variable + import PyQt5.QtCore except ImportError as e: text = _missing_str('PyQt5') text = text.replace('', '') @@ -187,7 +187,6 @@ def check_qt_version(): def check_ssl_support(): """Check if SSL support is available.""" - # pylint: disable=unused-variable try: from PyQt5.QtNetwork import QSslSocket except ImportError: diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index c634eb95f..6905c2fbc 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -285,7 +285,7 @@ class PyQIODevice(io.BufferedIOBase): if not ok: raise QtOSError(self.dev, msg="seek failed!") - def truncate(self, size=None): # pylint: disable=unused-argument + def truncate(self, size=None): raise io.UnsupportedOperation @property diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index e3f039336..6749be676 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -370,7 +370,7 @@ def main(): 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 + import github3 read_github_token() if args.no_asciidoc: diff --git a/tests/conftest.py b/tests/conftest.py index f53a70054..d06dfcfa6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -206,7 +206,6 @@ def pytest_configure(config): webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', '') config.webengine = bool(webengine_arg or webengine_env) # Fail early if QtWebEngine is not available - # pylint: disable=unused-variable if config.webengine: import PyQt5.QtWebEngineWidgets @@ -283,7 +282,7 @@ def apply_fake_os(monkeypatch, request): def check_yaml_c_exts(): """Make sure PyYAML C extensions are available on Travis.""" if 'TRAVIS' in os.environ: - from yaml import CLoader # pylint: disable=unused-variable + from yaml import CLoader @pytest.hookimpl(tryfirst=True, hookwrapper=True) diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 17e910a5f..15fc31d18 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -73,7 +73,7 @@ def test_patched_errwindow(capfd, mocker, monkeypatch): monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) try: - import tkinter # pylint: disable=unused-variable + import tkinter except ImportError: tk_mock = mocker.patch('qutebrowser.misc.checkpyver.Tk', spec=['withdraw'], new_callable=mocker.Mock) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index ccabfa050..37bd9faaa 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -463,9 +463,7 @@ class TestPreventExceptions: def test_raising(self, caplog): """Test with a raising function.""" with caplog.at_level(logging.ERROR, 'misc'): - # pylint: disable=assignment-from-no-return ret = self.func_raising() - # pylint: enable=assignment-from-no-return assert ret == 42 expected = 'Error in test_utils.TestPreventExceptions.func_raising' assert caplog.messages == [expected] @@ -488,9 +486,7 @@ class TestPreventExceptions: def test_predicate_true(self, caplog): """Test with a True predicate.""" with caplog.at_level(logging.ERROR, 'misc'): - # pylint: disable=assignment-from-no-return ret = self.func_predicate_true() - # enable: disable=assignment-from-no-return assert ret == 42 assert len(caplog.records) == 1 From 14fe7f9b0b7683b5024366e73d388900cfbcbb05 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 12:58:48 +0100 Subject: [PATCH 074/258] pass fixup --- tests/unit/commands/test_cmdutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index ef7c97164..718695874 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -376,6 +376,7 @@ class TestArgument: @cmdutils.register() def fun(): # no docstring + pass class TestRun: From b5253ec47381ef5c86e964375d4b26e0ffd46729 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 13:01:43 +0100 Subject: [PATCH 075/258] Fix lint --- qutebrowser/app.py | 2 +- qutebrowser/misc/backendproblem.py | 1 + qutebrowser/misc/earlyinit.py | 4 ++-- scripts/dev/build_release.py | 2 +- scripts/dev/src2asciidoc.py | 2 +- tests/end2end/fixtures/webserver.py | 3 ++- tests/unit/browser/test_pdfjs.py | 4 ++-- tests/unit/browser/webkit/http/test_http_hypothesis.py | 2 +- tests/unit/misc/test_checkpyver.py | 2 +- 9 files changed, 12 insertions(+), 10 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 7d1b61131..161bc5297 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -193,7 +193,7 @@ def _init_icon(): icon = QIcon() fallback_icon = QIcon() for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]: - filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size) + filename = ':/icons/qutebrowser-{size}x{size}.png'.format(size=size) pixmap = QPixmap(filename) if pixmap.isNull(): log.init.warning("Failed to load {}".format(filename)) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 53094a257..2099afa88 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -280,6 +280,7 @@ class BackendImports: def _try_import_backends(): """Check whether backends can be imported and return BackendImports.""" + # pylint: disable=unused-import results = BackendImports() try: diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 5e650510c..a7571fdaa 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -133,7 +133,7 @@ def init_faulthandler(fileobj=sys.__stderr__): def check_pyqt_core(): """Check if PyQt core is installed.""" try: - import PyQt5.QtCore + import PyQt5.QtCore # pylint: disable=unused-import except ImportError as e: text = _missing_str('PyQt5') text = text.replace('', '') @@ -188,7 +188,7 @@ def check_qt_version(): def check_ssl_support(): """Check if SSL support is available.""" try: - from PyQt5.QtNetwork import QSslSocket + from PyQt5.QtNetwork import QSslSocket # pylint: disable=unused-import except ImportError: _die("Fatal error: Your Qt is built without SSL support.") diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 6749be676..8591f1c31 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -370,7 +370,7 @@ def main(): if args.upload is not None: # Fail early when trying to upload without github3 installed # or without API token - import github3 + import github3 # pylint: disable=unused-import read_github_token() if args.no_asciidoc: diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index cc00c3757..8c6f2e44d 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -135,7 +135,7 @@ def _get_command_quickref(cmds): out.append('|Command|Description') for name, cmd in cmds: desc = inspect.getdoc(cmd.handler).splitlines()[0] - out.append('|<<{},{}>>|{}'.format(name, name, desc)) + out.append('|<<{name},{name}>>|{desc}'.format(name=name, desc=desc)) out.append('|==============') return '\n'.join(out) diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 254b5ffaf..d1e45409e 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -82,7 +82,8 @@ class Request(testprocess.Line): for i in range(15): path_to_statuses['/redirect/{}'.format(i)] = [HTTPStatus.FOUND] for suffix in ['', '1', '2', '3', '4', '5', '6']: - key = '/basic-auth/user{}/password{}'.format(suffix, suffix) + key = ('/basic-auth/user{suffix}/password{suffix}' + .format(suffix=suffix)) path_to_statuses[key] = [HTTPStatus.UNAUTHORIZED, HTTPStatus.OK] default_statuses = [HTTPStatus.OK, HTTPStatus.NOT_MODIFIED] diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index 7fa1a8f6c..dcee2b82b 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -82,14 +82,14 @@ def test_generate_pdfjs_script_disable_object_url(monkeypatch, if qt == 'new': monkeypatch.setattr(pdfjs.qtutils, 'version_check', lambda version, exact=False, compiled=True: - False if version == '5.7.1' else True) + version != '5.7.1') elif qt == 'old': monkeypatch.setattr(pdfjs.qtutils, 'version_check', lambda version, exact=False, compiled=True: False) elif qt == '5.7': monkeypatch.setattr(pdfjs.qtutils, 'version_check', lambda version, exact=False, compiled=True: - True if version == '5.7.1' else False) + version == '5.7.1') else: raise utils.Unreachable diff --git a/tests/unit/browser/webkit/http/test_http_hypothesis.py b/tests/unit/browser/webkit/http/test_http_hypothesis.py index ec8ee4aff..5727bf1bc 100644 --- a/tests/unit/browser/webkit/http/test_http_hypothesis.py +++ b/tests/unit/browser/webkit/http/test_http_hypothesis.py @@ -31,7 +31,7 @@ from qutebrowser.browser.webkit import http, rfc6266 'attachment; filename="{}"', 'inline; {}', 'attachment; {}="foo"', - 'attachment; filename*=iso-8859-1''{}', + "attachment; filename*=iso-8859-1''{}", 'attachment; filename*={}', ]) @hypothesis.given(strategies.text(alphabet=[chr(x) for x in range(255)])) diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 15fc31d18..a02e2f8e0 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -73,7 +73,7 @@ def test_patched_errwindow(capfd, mocker, monkeypatch): monkeypatch.setattr(checkpyver.sys, 'exit', lambda status: None) try: - import tkinter + import tkinter # pylint: disable=unused-import except ImportError: tk_mock = mocker.patch('qutebrowser.misc.checkpyver.Tk', spec=['withdraw'], new_callable=mocker.Mock) From 8765ebef230c6b12eeff11197ebf694cc03cf895 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 16:06:25 +0100 Subject: [PATCH 076/258] mypy: Don't claim to be a QWebEngineView --- qutebrowser/browser/browsertab.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index f3a613844..15cfb4f91 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -30,10 +30,6 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QApplication, QDialog from PyQt5.QtPrintSupport import QPrintDialog, QPrinter from PyQt5.QtNetwork import QNetworkAccessManager -MYPY = False -if MYPY: - # pylint: disable=unused-import - from PyQt5.QtWebEngineWidgets import QWebEngineView import pygments import pygments.lexers @@ -46,6 +42,7 @@ from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, from qutebrowser.misc import miscwidgets, objects from qutebrowser.browser import mouse, hints, webelem from qutebrowser.qt import sip +MYPY = False if MYPY: # pylint: disable=unused-import from qutebrowser.browser.inspector import AbstractWebInspector @@ -153,7 +150,7 @@ class AbstractAction: action_base = None # type: type def __init__(self, tab: 'AbstractTab') -> None: - self._widget = typing.cast('QWebEngineView', None) + self._widget = typing.cast(QWidget, None) self._tab = tab def exit_fullscreen(self) -> None: @@ -169,7 +166,7 @@ class AbstractAction: member = getattr(self.action_class, name, None) if not isinstance(member, self.action_base): raise WebTabError("{} is not a valid web action!".format(name)) - self._widget.triggerPageAction(member) # type: ignore + self._widget.triggerPageAction(member) def show_source( self, @@ -521,7 +518,7 @@ class AbstractScroller(QObject): def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) self._tab = tab - self._widget = None # type: typing.Optional[QWebEngineView] + self._widget = None # type: typing.Optional[QWidget] self.perc_changed.connect(self._log_scroll_pos_change) @pyqtSlot() @@ -529,7 +526,7 @@ class AbstractScroller(QObject): log.webview.vdebug( # type: ignore "Scroll position changed to {}".format(self.pos_px())) - def _init_widget(self, widget: 'QWebEngineView') -> None: + def _init_widget(self, widget: QWidget) -> None: self._widget = widget def pos_px(self) -> int: @@ -712,7 +709,7 @@ class AbstractAudio(QObject): def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) - self._widget = None # type: typing.Optional[QWebEngineView] + self._widget = None # type: typing.Optional[QWidget] self._tab = tab def set_muted(self, muted: bool, override: bool = False) -> None: @@ -806,7 +803,7 @@ class AbstractTab(QWidget): self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) - self._widget = None # type: typing.Optional[QWebEngineView] + self._widget = None # type: typing.Optional[QWidget] self._progress = 0 self._has_ssl_errors = False self._mode_manager = mode_manager @@ -823,7 +820,7 @@ class AbstractTab(QWidget): self.predicted_navigation.connect(self._on_predicted_navigation) - def _set_widget(self, widget: 'QWebEngineView') -> None: + def _set_widget(self, widget: QWidget) -> None: # pylint: disable=protected-access self._widget = widget self._layout.wrap(self, widget) From d60dff26233d777dc8eca888ac60f02737d80d54 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 17:34:33 +0100 Subject: [PATCH 077/258] Renaem private attribute to is_private --- qutebrowser/browser/browsertab.py | 4 ++-- qutebrowser/browser/commands.py | 6 +++--- qutebrowser/browser/navigate.py | 2 +- qutebrowser/browser/shared.py | 2 +- qutebrowser/browser/webelem.py | 2 +- qutebrowser/mainwindow/statusbar/bar.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 11 ++++++----- qutebrowser/mainwindow/tabwidget.py | 2 +- qutebrowser/misc/sessions.py | 4 ++-- 9 files changed, 18 insertions(+), 17 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 15cfb4f91..ab186e56a 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -743,7 +743,7 @@ class AbstractTab(QWidget): Attributes: history: The AbstractHistory for the current tab. registry: The ObjectRegistry associated with this tab. - private: Whether private browsing is turned on for this tab. + is_private: Whether private browsing is turned on for this tab. _load_status: loading status of this page Accessible via load_status() method. @@ -790,7 +790,7 @@ class AbstractTab(QWidget): mode_manager: modeman.ModeManager, private: bool, parent: QWidget = None) -> None: - self.private = private + self.is_private = private self.win_id = win_id self.tab_id = next(tab_id_gen) super().__init__(parent) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index a53cfc75e..2b4165364 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -126,7 +126,7 @@ class CommandDispatcher: tabbed_browser = self._tabbed_browser cmdutils.check_exclusive((tab, background, window, private), 'tbwp') if window and private is None: - private = self._tabbed_browser.private + private = self._tabbed_browser.is_private if window or private: tabbed_browser = self._new_tabbed_browser(private) @@ -470,7 +470,7 @@ class CommandDispatcher: # tabs.tabs_are_windows being set) if window: new_tabbed_browser = self._new_tabbed_browser( - private=self._tabbed_browser.private) + private=self._tabbed_browser.is_private) else: new_tabbed_browser = self._tabbed_browser newtab = new_tabbed_browser.tabopen(background=bg) @@ -536,7 +536,7 @@ class CommandDispatcher: "only one tab") tabbed_browser = self._new_tabbed_browser( - private=self._tabbed_browser.private) + private=self._tabbed_browser.is_private) else: if win_id not in objreg.window_registry: raise cmdexc.CommandError( diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index baeb7bf5b..0f80775bd 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -140,7 +140,7 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, if window: new_window = mainwindow.MainWindow( - private=cur_tabbed_browser.private) + private=cur_tabbed_browser.is_private) new_window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=new_window.win_id) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index eb222cbe8..0bf3301f9 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -262,7 +262,7 @@ def get_tab(win_id, target): elif target == usertypes.ClickTarget.window: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - window = mainwindow.MainWindow(private=tabbed_browser.private) + window = mainwindow.MainWindow(private=tabbed_browser.is_private) window.show() win_id = window.win_id bg_tab = False diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 520b7d12b..a22facfbd 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -382,7 +382,7 @@ class AbstractWebElement(collections.abc.MutableMapping): background = click_target == usertypes.ClickTarget.tab_bg tabbed_browser.tabopen(url, background=background) elif click_target == usertypes.ClickTarget.window: - window = mainwindow.MainWindow(private=tabbed_browser.private) + window = mainwindow.MainWindow(private=tabbed_browser.is_private) window.show() window.tabbed_browser.tabopen(url) else: diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 3edb8128a..13a368f05 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -367,7 +367,7 @@ class StatusBar(QWidget): self.percentage.on_tab_changed(tab) self.backforward.on_tab_changed(tab) self.maybe_hide() - assert tab.private == self._color_flags.private + assert tab.is_private == self._color_flags.private @pyqtSlot(bool) def on_caret_selection_toggled(self, selection): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 1f3484663..15b8f2c7b 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -76,7 +76,7 @@ class TabbedBrowser(QWidget): _local_marks: Jump markers local to each page _global_marks: Jump markers used across all pages default_window_icon: The qutebrowser window icon - private: Whether private browsing is on for this window. + is_private: Whether private browsing is on for this window. Signals: cur_progress: Progress of the current tab changed (load_progress). @@ -131,7 +131,7 @@ class TabbedBrowser(QWidget): self._local_marks = {} self._global_marks = {} self.default_window_icon = self.widget.window().windowIcon() - self.private = private + self.is_private = private config.instance.changed.connect(self._on_config_changed) def __repr__(self): @@ -243,7 +243,7 @@ class TabbedBrowser(QWidget): tab.audio.recently_audible_changed.connect( functools.partial(self._on_audio_changed, tab)) tab.new_tab_requested.connect(self.tabopen) - if not self.private: + if not self.is_private: web_history = objreg.get('web-history') tab.add_history_item.connect(web_history.add_from_tab) @@ -466,14 +466,15 @@ class TabbedBrowser(QWidget): if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and not ignore_tabs_are_windows): - window = mainwindow.MainWindow(private=self.private) + window = mainwindow.MainWindow(private=self.is_private) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) return tabbed_browser.tabopen(url=url, background=background, related=related) - tab = browsertab.create(win_id=self._win_id, private=self.private, + tab = browsertab.create(win_id=self._win_id, + private=self.is_private, parent=self.widget) self._connect_tab_signals(tab) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index a3ba0f1da..ca4be6d50 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -177,7 +177,7 @@ class TabWidget(QTabWidget): fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() fields['backend'] = objects.backend.name - fields['private'] = ' [Private Mode] ' if tab.private else '' + fields['private'] = ' [Private Mode] ' if tab.is_private else '' try: if tab.audio.is_muted(): fields['audio'] = TabWidget.MUTE_STRING diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index e50d803a2..5f50bf5d1 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -242,7 +242,7 @@ class SessionManager(QObject): if sip.isdeleted(main_window): continue - if tabbed_browser.private and not with_private: + if tabbed_browser.is_private and not with_private: continue win_data = {} @@ -251,7 +251,7 @@ class SessionManager(QObject): win_data['active'] = True win_data['geometry'] = bytes(main_window.saveGeometry()) win_data['tabs'] = [] - if tabbed_browser.private: + if tabbed_browser.is_private: win_data['private'] = True for i, tab in enumerate(tabbed_browser.widgets()): active = i == tabbed_browser.widget.currentIndex() From 5f5f20209833dfcbbd2d09bc5ae378956863d7a8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 17:59:27 +0100 Subject: [PATCH 078/258] Move private tab API into an own object --- qutebrowser/browser/browsertab.py | 116 ++++++++++-------- qutebrowser/browser/commands.py | 4 +- qutebrowser/browser/hints.py | 2 +- qutebrowser/browser/mouse.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 44 ++++--- qutebrowser/browser/webkit/webkittab.py | 45 ++++--- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- tests/helpers/stubs.py | 10 +- 8 files changed, 127 insertions(+), 98 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ab186e56a..86cca09f1 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -734,6 +734,67 @@ class AbstractAudio(QObject): raise NotImplementedError +class AbstractTabPrivate: + + """Tab-related methods which are only needed in the core. + + Those methods are not part of the API which is exposed to extensions, and + should ideally be removed at some point in the future. + """ + + def __init__(self, mode_manager: modeman.ModeManager, + tab: 'AbstractTab') -> None: + self._widget = None # type: typing.Optional[QWidget] + self._tab = tab + self._mode_manager = mode_manager + + def event_target(self) -> QWidget: + """Return the widget events should be sent to.""" + raise NotImplementedError + + def handle_auto_insert_mode(self, ok: bool) -> None: + """Handle `input.insert_mode.auto_load` after loading finished.""" + if not config.val.input.insert_mode.auto_load or not ok: + return + + cur_mode = self._mode_manager.mode + if cur_mode == usertypes.KeyMode.insert: + return + + def _auto_insert_mode_cb(elem: webelem.AbstractWebElement) -> None: + """Called from JS after finding the focused element.""" + if elem is None: + log.webview.debug("No focused element!") + return + if elem.is_editable(): + modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, + 'load finished', only_if_normal=True) + + self._tab.elements.find_focused(_auto_insert_mode_cb) + + def clear_ssl_errors(self) -> None: + raise NotImplementedError + + def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]: + """Get the QNetworkAccessManager for this tab. + + This is only implemented for QtWebKit. + For QtWebEngine, always returns None. + """ + raise NotImplementedError + + def user_agent(self) -> typing.Optional[str]: + """Get the user agent for this tab. + + This is only implemented for QtWebKit. + For QtWebEngine, always returns None. + """ + raise NotImplementedError + + def shutdown(self) -> None: + raise NotImplementedError + + class AbstractTab(QWidget): """A wrapper over the given widget to hide its API and expose another one. @@ -785,10 +846,7 @@ class AbstractTab(QWidget): renderer_process_terminated = pyqtSignal(TerminationStatus, int) predicted_navigation = pyqtSignal(QUrl) - def __init__(self, *, - win_id: int, - mode_manager: modeman.ModeManager, - private: bool, + def __init__(self, *, win_id: int, private: bool, parent: QWidget = None) -> None: self.is_private = private self.win_id = win_id @@ -806,7 +864,6 @@ class AbstractTab(QWidget): self._widget = None # type: typing.Optional[QWidget] self._progress = 0 self._has_ssl_errors = False - self._mode_manager = mode_manager self._load_status = usertypes.LoadStatus.none self._mouse_event_filter = mouse.MouseEventFilter( self, parent=self) @@ -833,6 +890,7 @@ class AbstractTab(QWidget): self.action._widget = widget self.elements._widget = widget self.audio._widget = widget + self.private_api._widget = widget self.settings._settings = widget.settings() self._install_event_filter() @@ -849,10 +907,6 @@ class AbstractTab(QWidget): self._load_status = val self.load_status_changed.emit(val.name) - def event_target(self) -> QWidget: - """Return the widget events should be sent to.""" - raise NotImplementedError - def send_event(self, evt: QEvent) -> None: """Send the given event to the underlying widget. @@ -865,7 +919,7 @@ class AbstractTab(QWidget): raise utils.Unreachable("Can't re-use an event which was already " "posted!") - recipient = self.event_target() + recipient = self.private_api.event_target() if recipient is None: # https://github.com/qutebrowser/qutebrowser/issues/3888 log.webview.warning("Unable to find event target!") @@ -925,26 +979,6 @@ class AbstractTab(QWidget): navigation.url.errorString())) navigation.accepted = False - def handle_auto_insert_mode(self, ok: bool) -> None: - """Handle `input.insert_mode.auto_load` after loading finished.""" - if not config.val.input.insert_mode.auto_load or not ok: - return - - cur_mode = self._mode_manager.mode - if cur_mode == usertypes.KeyMode.insert: - return - - def _auto_insert_mode_cb(elem: webelem.AbstractWebElement) -> None: - """Called from JS after finding the focused element.""" - if elem is None: - log.webview.debug("No focused element!") - return - if elem.is_editable(): - modeman.enter(self.win_id, usertypes.KeyMode.insert, - 'load finished', only_if_normal=True) - - self.elements.find_focused(_auto_insert_mode_cb) - @pyqtSlot(bool) def _on_load_finished(self, ok: bool) -> None: assert self._widget is not None @@ -1010,9 +1044,6 @@ class AbstractTab(QWidget): def stop(self) -> None: raise NotImplementedError - def clear_ssl_errors(self) -> None: - raise NotImplementedError - def key_press(self, key: Qt.Key, modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: @@ -1048,9 +1079,6 @@ class AbstractTab(QWidget): """ raise NotImplementedError - def shutdown(self) -> None: - raise NotImplementedError - def title(self) -> str: raise NotImplementedError @@ -1060,22 +1088,6 @@ class AbstractTab(QWidget): def set_html(self, html: str, base_url: QUrl = QUrl()) -> None: raise NotImplementedError - def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]: - """Get the QNetworkAccessManager for this tab. - - This is only implemented for QtWebKit. - For QtWebEngine, always returns None. - """ - raise NotImplementedError - - def user_agent(self) -> typing.Optional[str]: - """Get the user agent for this tab. - - This is only implemented for QtWebKit. - For QtWebEngine, always returns None. - """ - raise NotImplementedError - def __repr__(self) -> str: try: qurl = self.url() diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2b4165364..1a69ef137 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1516,7 +1516,7 @@ class CommandDispatcher: else: download_manager.get_mhtml(tab, target) else: - qnam = tab.networkaccessmanager() + qnam = tab.private_api.networkaccessmanager() suggested_fn = downloads.suggested_fn_from_title( self._current_url().path(), tab.title() @@ -2165,7 +2165,7 @@ class CommandDispatcher: debug=True, backend=usertypes.Backend.QtWebKit) def debug_clear_ssl_errors(self): """Clear remembered SSL error answers.""" - self._current_widget().clear_ssl_errors() + self._current_widget().private_api.clear_ssl_errors() @cmdutils.register(instance='command-dispatcher', scope='window') def edit_url(self, url=None, bg=False, tab=False, window=False, diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index aa5b5f34c..0374c7f1f 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -304,7 +304,7 @@ class HintActions: raise HintingError("No suitable link found for this element.") prompt = False if context.rapid else None - qnam = context.tab.networkaccessmanager() + qnam = context.tab.private_api.networkaccessmanager() user_agent = context.tab.user_agent() # FIXME:qtwebengine do this with QtWebEngine downloads? diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 7c405a57e..a73f28203 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -240,7 +240,7 @@ class MouseEventFilter(QObject): evtype = event.type() if evtype not in self._handlers: return False - if obj is not self._tab.event_target(): + if obj is not self._tab.private_api.event_target(): log.mouse.debug("Ignoring {} to {}".format( event.__class__.__name__, obj)) return False diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 47bacd60e..63f1201da 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1038,6 +1038,28 @@ class _WebEngineScripts(QObject): page_scripts.insert(new_script) +class WebEngineTabPrivate(browsertab.AbstractTabPrivate): + + """QtWebEngine-related methods which aren't part of the public API.""" + + def networkaccessmanager(self): + return None + + def user_agent(self): + return None + + def clear_ssl_errors(self): + raise browsertab.UnsupportedOperationError + + def event_target(self): + return self._widget.render_widget() + + def shutdown(self): + self._tab.shutting_down.emit() + self._tab.action.exit_fullscreen() + self._widget.shutdown() + + class WebEngineTab(browsertab.AbstractTab): """A QtWebEngine tab in the browser. @@ -1051,8 +1073,7 @@ class WebEngineTab(browsertab.AbstractTab): _load_finished_fake = pyqtSignal(bool) def __init__(self, *, win_id, mode_manager, private, parent=None): - super().__init__(win_id=win_id, mode_manager=mode_manager, - private=private, parent=parent) + super().__init__(win_id=win_id, private=private, parent=parent) widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, private=private) self.history = WebEngineHistory(tab=self) @@ -1065,6 +1086,8 @@ class WebEngineTab(browsertab.AbstractTab): self.elements = WebEngineElements(tab=self) self.action = WebEngineAction(tab=self) self.audio = WebEngineAudio(tab=self, parent=self) + self.private_api = WebEngineTabPrivate(mode_manager=mode_manager, + tab=self) self._permissions = _WebEnginePermissions(tab=self, parent=self) self._scripts = _WebEngineScripts(tab=self, parent=self) # We're assigning settings in _set_widget @@ -1146,11 +1169,6 @@ class WebEngineTab(browsertab.AbstractTab): else: self._widget.page().runJavaScript(code, world_id, callback) - def shutdown(self): - self.shutting_down.emit() - self.action.exit_fullscreen() - self._widget.shutdown() - def reload(self, *, force=False): if force: action = QWebEnginePage.ReloadAndBypassCache @@ -1175,15 +1193,6 @@ class WebEngineTab(browsertab.AbstractTab): # percent encoded content is 2 megabytes minus 30 bytes. self._widget.setHtml(html, base_url) - def networkaccessmanager(self): - return None - - def user_agent(self): - return None - - def clear_ssl_errors(self): - raise browsertab.UnsupportedOperationError - def key_press(self, key, modifier=Qt.NoModifier): press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, @@ -1485,6 +1494,3 @@ class WebEngineTab(browsertab.AbstractTab): self.audio._connect_signals() self._permissions.connect_signals() self._scripts.connect_signals() - - def event_target(self): - return self._widget.render_widget() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 51e3f385e..dcac1a3cb 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -657,13 +657,33 @@ class WebKitAudio(browsertab.AbstractAudio): return False +class WebKitTabPrivate(browsertab.AbstractTabPrivate): + + """QtWebKit-related methods which aren't part of the public API.""" + + def networkaccessmanager(self): + return self._widget.page().networkAccessManager() + + def user_agent(self): + page = self._widget.page() + return page.userAgentForUrl(self._tab.url()) + + def clear_ssl_errors(self): + self.networkaccessmanager().clear_all_ssl_errors() + + def event_target(self): + return self._widget + + def shutdown(self): + self._widget.shutdown() + + class WebKitTab(browsertab.AbstractTab): """A QtWebKit tab in the browser.""" def __init__(self, *, win_id, mode_manager, private, parent=None): - super().__init__(win_id=win_id, mode_manager=mode_manager, - private=private, parent=parent) + super().__init__(win_id=win_id, private=private, parent=parent) widget = webview.WebView(win_id=win_id, tab_id=self.tab_id, private=private, tab=self) if private: @@ -678,6 +698,8 @@ class WebKitTab(browsertab.AbstractTab): self.elements = WebKitElements(tab=self) self.action = WebKitAction(tab=self) self.audio = WebKitAudio(tab=self, parent=self) + self.private_api = WebKitTabPrivate(mode_manager=mode_manager, + tab=self) # We're assigning settings in _set_widget self.settings = webkitsettings.WebKitSettings(settings=None) self._set_widget(widget) @@ -720,9 +742,6 @@ class WebKitTab(browsertab.AbstractTab): def icon(self): return self._widget.icon() - def shutdown(self): - self._widget.shutdown() - def reload(self, *, force=False): if force: action = QWebPage.ReloadAndBypassCache @@ -736,9 +755,6 @@ class WebKitTab(browsertab.AbstractTab): def title(self): return self._widget.title() - def clear_ssl_errors(self): - self.networkaccessmanager().clear_all_ssl_errors() - def key_press(self, key, modifier=Qt.NoModifier): press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, @@ -755,17 +771,11 @@ class WebKitTab(browsertab.AbstractTab): def set_html(self, html, base_url=QUrl()): self._widget.setHtml(html, base_url) - def networkaccessmanager(self): - return self._widget.page().networkAccessManager() - - def user_agent(self): - page = self._widget.page() - return page.userAgentForUrl(self.url()) - @pyqtSlot() def _on_load_started(self): super()._on_load_started() - self.networkaccessmanager().netrc_used = False + nam = self._widget.page().networkAccessManager() + nam.netrc_used = False # Make sure the icon is cleared when navigating to a page without one. self.icon_changed.emit(QIcon()) @@ -847,6 +857,3 @@ class WebKitTab(browsertab.AbstractTab): frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.initialLayoutCompleted.connect(self._on_history_trigger) page.navigation_request.connect(self._on_navigation_request) - - def event_target(self): - return self._widget diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 15b8f2c7b..28f911aa9 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -753,7 +753,7 @@ class TabbedBrowser(QWidget): self.widget.update_tab_title(idx) if idx == self.widget.currentIndex(): self._update_window_title() - tab.handle_auto_insert_mode(ok) + tab.private_api.handle_auto_insert_mode(ok) @pyqtSlot() def on_scroll_pos_changed(self): diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index e050e1fb5..6475e21d8 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -241,6 +241,12 @@ class FakeWebTabAudio(browsertab.AbstractAudio): return False +class FakeWebTabPrivate: + + def shutdown(self): + pass + + class FakeWebTab(browsertab.AbstractTab): """Fake AbstractTab to use in tests.""" @@ -258,6 +264,7 @@ class FakeWebTab(browsertab.AbstractTab): can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) self.audio = FakeWebTabAudio(self) + self.private_api = FakeWebTabPrivate() wrapped = QWidget() self._layout.wrap(self, wrapped) @@ -274,9 +281,6 @@ class FakeWebTab(browsertab.AbstractTab): def load_status(self): return self._load_status - def shutdown(self): - pass - def icon(self): return QIcon() From 89650d89f1af0d1a7d783d3f3396c106bb699d11 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 18:01:04 +0100 Subject: [PATCH 079/258] Fix lint --- misc/requirements/requirements-optional.txt | 2 +- qutebrowser/browser/browsertab.py | 3 ++- qutebrowser/browser/webkit/network/networkmanager.py | 7 ++++++- qutebrowser/misc/sessions.py | 2 -- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/misc/requirements/requirements-optional.txt b/misc/requirements/requirements-optional.txt index 6e1e8b8ff..8f1e2498a 100644 --- a/misc/requirements/requirements-optional.txt +++ b/misc/requirements/requirements-optional.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -colorama==0.4.0 +colorama==0.4.1 cssutils==1.0.2 hunter==2.1.0 Pympler==0.6 diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 86cca09f1..1ced63fab 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -44,7 +44,8 @@ from qutebrowser.browser import mouse, hints, webelem from qutebrowser.qt import sip MYPY = False if MYPY: - # pylint: disable=unused-import + # pylint can't interpret type comments with Python 3.7 + # pylint: disable=unused-import,useless-suppression from qutebrowser.browser.inspector import AbstractWebInspector diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 0c3148ee4..2ca1ae0d9 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -29,7 +29,12 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from qutebrowser.config import config -from qutebrowser.mainwindow import prompt # pylint: disable=unused-import + +MYPY = False +if MYPY: + # pylint can't interpret type comments with Python 3.7 + # pylint: disable=unused-import,useless-suppression + from qutebrowser.mainwindow import prompt from qutebrowser.utils import (message, log, usertypes, utils, objreg, urlutils, debug) from qutebrowser.browser import shared diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 5f50bf5d1..459011e0f 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -42,8 +42,6 @@ class Sentinel: """Sentinel value for default argument.""" - pass - default = Sentinel() From 1d4729c7f735b2e50da353417ac0d18a35524638 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 19:56:52 +0100 Subject: [PATCH 080/258] Fix shutdown call --- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 28f911aa9..d00d5d797 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -356,7 +356,7 @@ class TabbedBrowser(QWidget): else: self._undo_stack[-1].append(entry) - tab.shutdown() + tab.private_api.shutdown() self.widget.removeTab(idx) if not crashed: # WORKAROUND for a segfault when we delete the crashed tab. From ee2461a6fe4956d0e9e9b5091e22767287b2f8d5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 21:57:01 +0100 Subject: [PATCH 081/258] Fix accessing user_agent() --- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/hints.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1a69ef137..53efcfdf2 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1495,7 +1495,7 @@ class CommandDispatcher: target = downloads.FileDownloadTarget(dest) tab = self._current_widget() - user_agent = tab.user_agent() + user_agent = tab.private_api.user_agent() if url: if mhtml_: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 0374c7f1f..730128295 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -305,7 +305,7 @@ class HintActions: prompt = False if context.rapid else None qnam = context.tab.private_api.networkaccessmanager() - user_agent = context.tab.user_agent() + user_agent = context.tab.private_api.user_agent() # FIXME:qtwebengine do this with QtWebEngine downloads? download_manager = objreg.get('qtnetwork-download-manager') From cbf55e1ef91d164391a992b0427eda649257ed02 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Nov 2018 22:19:07 +0100 Subject: [PATCH 082/258] Fix remaining private_api refactoring issues --- tests/helpers/stubs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 6475e21d8..ed6fce36c 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -241,7 +241,7 @@ class FakeWebTabAudio(browsertab.AbstractAudio): return False -class FakeWebTabPrivate: +class FakeWebTabPrivate(browsertab.AbstractTabPrivate): def shutdown(self): pass @@ -255,7 +255,7 @@ class FakeWebTab(browsertab.AbstractTab): scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, progress=0, can_go_back=None, can_go_forward=None): - super().__init__(win_id=0, mode_manager=None, private=False) + super().__init__(win_id=0, private=False) self._load_status = load_status self._title = title self._url = url @@ -264,7 +264,7 @@ class FakeWebTab(browsertab.AbstractTab): can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) self.audio = FakeWebTabAudio(self) - self.private_api = FakeWebTabPrivate() + self.private_api = FakeWebTabPrivate(tab=self, mode_manager=None) wrapped = QWidget() self._layout.wrap(self, wrapped) From a7a1cc7c137c3bcafee53332ea8a70e5ac4bed1d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 00:49:22 -0600 Subject: [PATCH 083/258] Update doc/help/configuring.asciidoc Co-Authored-By: winny- --- doc/help/configuring.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 9e6f2a3c7..9023d1dc3 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -229,7 +229,7 @@ Loading `autoconfig.yml` ~~~~~~~~~~~~~~~~~~~~~~~~ All customization done via the UI (`:set`, `:bind` and `:unbind`) is -stored in file `autoconfig.yml`. When file `config.py` exists, `autoconfig.yml` +stored in the `autoconfig.yml` file. When a `config.py` file exists, `autoconfig.yml` is not loaded automatically. To load `autoconfig.yml` automatically, add the following snippet to `config.py`: From 13532aca18534023b77975b1fcbed56f89ae39be Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 00:49:32 -0600 Subject: [PATCH 084/258] Update doc/help/configuring.asciidoc Co-Authored-By: winny- --- doc/help/configuring.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 9023d1dc3..33711b755 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -19,7 +19,7 @@ hand, you can simply use those - see <> for details. For more advanced configuration, you can write a `config.py` file - see -<>. When `config.py` +<>. When a `config.py` exists, the `autoconfig.yml` file **is not read anymore** by default. You need to <> if you want settings changed via `:set`/`:bind` to persist between restarts. From 31a122e97be807fa954920eaec38f826f8a74c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Wed, 21 Nov 2018 17:27:32 +0100 Subject: [PATCH 085/258] Encode slashes in search terms for searchengines If a search term contains a slash, this should be encoded. While this makes no differences for search engines of the form http://example.org?q={} it does for engines like these: http://example.org/search/{} For a real world example, try: https://www.doi2bib.org/bib/{} --- qutebrowser/utils/urlutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 4dda6b3fc..6687b9834 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -116,7 +116,7 @@ def _get_search_url(txt): if engine is None: engine = 'DEFAULT' template = config.val.url.searchengines[engine] - url = qurl_from_user_input(template.format(urllib.parse.quote(term))) + url = qurl_from_user_input(template.format(urllib.parse.quote(term, safe=''))) if config.val.url.open_base_url and term in config.val.url.searchengines: url = qurl_from_user_input(config.val.url.searchengines[term]) From 65c51931c77a609a61147e7b8854ecfb523f9ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Thu, 22 Nov 2018 12:49:03 +0100 Subject: [PATCH 086/258] Wrap url quoting into new line --- qutebrowser/utils/urlutils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 6687b9834..00cf4d01a 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -116,7 +116,8 @@ def _get_search_url(txt): if engine is None: engine = 'DEFAULT' template = config.val.url.searchengines[engine] - url = qurl_from_user_input(template.format(urllib.parse.quote(term, safe=''))) + quoted_term = urllib.parse.quote(term, safe='') + url = qurl_from_user_input(template.format(quoted_term)) if config.val.url.open_base_url and term in config.val.url.searchengines: url = qurl_from_user_input(config.val.url.searchengines[term]) From 9d069ea12b409f5a24a62d76973989c8bdf43d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Wi=C3=9Fmann?= Date: Thu, 22 Nov 2018 13:31:39 +0100 Subject: [PATCH 087/258] Encode slashes in default search engine test --- tests/end2end/features/yankpaste.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index 806bbd2d2..08a62d302 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -187,10 +187,10 @@ Feature: Yanking and pasting. http://qutebrowser.org should not open And I run :open -t {clipboard} - And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded + And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A%2F%2Fqutebrowser.org%0Ashould%20not%20open is loaded Then the following tabs should be open: - about:blank - - data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open (active) + - data/hello.txt?q=this%20url%3A%0Ahttp%3A%2F%2Fqutebrowser.org%0Ashould%20not%20open (active) Scenario: Pasting multiline whose first line looks like a URI When I set url.auto_search to naive From 351b6c9b45366b609c861762e5c27623d486063e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 08:28:35 +0100 Subject: [PATCH 088/258] Add unit test for slashes in search terms --- tests/unit/utils/test_urlutils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index e77d33783..26b063456 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -97,6 +97,7 @@ def init_config(config_stub): config_stub.val.url.searchengines = { 'test': 'http://www.qutebrowser.org/?q={}', 'test-with-dash': 'http://www.example.org/?q={}', + 'path-search': 'http://www.example.org/{}', 'DEFAULT': 'http://www.example.com/?q={}', } @@ -288,6 +289,7 @@ def test_special_urls(url, special): ('blub testfoo', 'www.example.com', 'q=blub testfoo'), ('stripped ', 'www.example.com', 'q=stripped'), ('test-with-dash testfoo', 'www.example.org', 'q=testfoo'), + ('test/with/slashes', 'www.example.com', 'q=test%2Fwith%2Fslashes'), ]) def test_get_search_url(config_stub, url, host, query, open_base_url): """Test _get_search_url(). From c3477b0468451248057cbfc8afd2850ce8de22fd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 08:29:11 +0100 Subject: [PATCH 089/258] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 51647bafd..e4da4de4c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -67,6 +67,7 @@ Fixed to be in line with what other browsers do. - `: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. v1.5.2 ------ From 4caa2e056b8f9330e9af1d487cabc8b9d92a9f71 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 11:08:24 +0100 Subject: [PATCH 090/258] Add type annotations to misc.objects --- qutebrowser/misc/objects.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index d6c116eab..ec558aa37 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -22,14 +22,20 @@ # NOTE: We need to be careful with imports here, as this is imported from # earlyinit. +import typing + +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + from qutebrowser.utils import usertypes + class NoBackend: """Special object when there's no backend set so we notice that.""" - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: raise AssertionError("No backend set!") -# A usertypes.Backend member -backend = NoBackend() +backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend] From 168bc3dc494c75ed540b2c1eb3b5306b682481bb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 11:09:04 +0100 Subject: [PATCH 091/258] Be stricter in mypy.ini --- mypy.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy.ini b/mypy.ini index 94d5dff40..84a6fb239 100644 --- a/mypy.ini +++ b/mypy.ini @@ -52,3 +52,8 @@ disallow_subclassing_any = False [mypy-qutebrowser.browser.browsertab] disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.misc.objects] +disallow_untyped_defs = True +disallow_incomplete_defs = True From 19628d0ae99bb738ed4dbe1dea42f6ac95f9e301 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 11:21:07 +0100 Subject: [PATCH 092/258] Move cmdutils.cmd_dict to objects.commands --- qutebrowser/browser/commands.py | 4 +- qutebrowser/commands/cmdutils.py | 9 ++--- qutebrowser/commands/runners.py | 8 ++-- qutebrowser/completion/completer.py | 7 ++-- qutebrowser/completion/models/util.py | 6 +-- qutebrowser/config/configtypes.py | 4 +- qutebrowser/misc/keyhintwidget.py | 4 +- qutebrowser/misc/objects.py | 2 + scripts/dev/run_vulture.py | 4 +- scripts/dev/src2asciidoc.py | 5 ++- tests/helpers/stubs.py | 8 ---- tests/unit/commands/test_cmdutils.py | 52 ++++++++++++------------- tests/unit/commands/test_runners.py | 5 ++- tests/unit/completion/test_completer.py | 6 +-- tests/unit/completion/test_models.py | 4 +- tests/unit/config/test_configtypes.py | 9 +++-- tests/unit/misc/test_keyhints.py | 3 +- 17 files changed, 69 insertions(+), 71 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 53efcfdf2..e9d6b8b68 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -37,7 +37,7 @@ from qutebrowser.keyinput import modeman, keyutils from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils, standarddir) from qutebrowser.utils.usertypes import KeyMode -from qutebrowser.misc import editor, guiprocess +from qutebrowser.misc import editor, guiprocess, objects from qutebrowser.completion.models import urlmodel, miscmodels from qutebrowser.mainwindow import mainwindow @@ -1612,7 +1612,7 @@ class CommandDispatcher: path = 'index.html' elif topic.startswith(':'): command = topic[1:] - if command not in cmdutils.cmd_dict: + if command not in objects.commands: raise cmdexc.CommandError("Invalid command {}!".format( command)) path = 'commands.html#{}'.format(command) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 41e875202..12ffabe3a 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -22,11 +22,10 @@ import inspect import typing # pylint: disable=unused-import +from qutebrowser.misc import objects from qutebrowser.utils import qtutils, log from qutebrowser.commands import command, cmdexc -cmd_dict = {} # type: typing.Dict[str, command.Command] - def check_overflow(arg, ctype): """Check if the given argument is in bounds for the given type. @@ -89,7 +88,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name Gets called when a function should be decorated. Doesn't actually decorate anything, but creates a Command object and - registers it in the cmd_dict. + registers it in the global commands dict. Args: func: The function to be decorated. @@ -104,11 +103,11 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name name = self._name log.commands.vdebug("Registering command {} (from {}:{})".format( name, func.__module__, func.__qualname__)) - if name in cmd_dict: + if name in objects.commands: raise ValueError("{} is already registered!".format(name)) cmd = command.Command(name=name, instance=self._instance, handler=func, **self._kwargs) - cmd_dict[name] = cmd + objects.commands[name] = cmd return func diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index f1c7641e7..86b0cc3a2 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -26,9 +26,9 @@ import attr from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.config import config -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.commands import cmdexc from qutebrowser.utils import message, objreg, qtutils, usertypes, utils -from qutebrowser.misc import split +from qutebrowser.misc import split, objects last_command = {} @@ -193,7 +193,7 @@ class CommandParser: cmdstr = self._completion_match(cmdstr) try: - cmd = cmdutils.cmd_dict[cmdstr] + cmd = objects.commands[cmdstr] except KeyError: if not fallback: raise cmdexc.NoSuchCommandError( @@ -220,7 +220,7 @@ class CommandParser: Return: cmdstr modified to the matching completion or unmodified """ - matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len) + matches = [cmd for cmd in sorted(objects.commands, key=len) if cmdstr in cmd] if len(matches) == 1: cmdstr = matches[0] diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 4cbdc4724..ea64225d5 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -23,7 +23,8 @@ import attr from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import cmdutils, runners +from qutebrowser.commands import runners +from qutebrowser.misc import objects from qutebrowser.utils import log, utils, debug from qutebrowser.completion.models import miscmodels @@ -92,7 +93,7 @@ class Completer(QObject): log.completion.debug('Starting command completion') return miscmodels.command try: - cmd = cmdutils.cmd_dict[before_cursor[0]] + cmd = objects.commands[before_cursor[0]] except KeyError: log.completion.debug("No completion for unknown command: {}" .format(before_cursor[0])) @@ -170,7 +171,7 @@ class Completer(QObject): before, center, after = self._partition() log.completion.debug("Changing {} to '{}'".format(center, text)) try: - maxsplit = cmdutils.cmd_dict[before[0]].maxsplit + maxsplit = objects.commands[before[0]].maxsplit except (KeyError, IndexError): maxsplit = None if maxsplit is None: diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py index c1b8b56f9..08f99eb6c 100644 --- a/qutebrowser/completion/models/util.py +++ b/qutebrowser/completion/models/util.py @@ -20,7 +20,7 @@ """Utility functions for completion models.""" from qutebrowser.utils import objreg, usertypes -from qutebrowser.commands import cmdutils +from qutebrowser.misc import objects def get_cmd_completions(info, include_hidden, include_aliases, prefix=''): @@ -34,10 +34,10 @@ def get_cmd_completions(info, include_hidden, include_aliases, prefix=''): Return: A list of tuples of form (name, description, bindings). """ - assert cmdutils.cmd_dict + assert objects.commands cmdlist = [] cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal') - for obj in set(cmdutils.cmd_dict.values()): + for obj in set(objects.commands.values()): hide_debug = obj.debug and not objreg.get('args').debug hide_mode = (usertypes.KeyMode.normal not in obj.modes and not include_hidden) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 31eca988e..691584801 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -60,7 +60,7 @@ from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar -from qutebrowser.commands import cmdutils +from qutebrowser.misc import objects from qutebrowser.config import configexc, configutils from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch from qutebrowser.keyinput import keyutils @@ -881,7 +881,7 @@ class Command(BaseType): def complete(self): out = [] - for cmdname, obj in cmdutils.cmd_dict.items(): + for cmdname, obj in objects.commands.items(): out.append((cmdname, obj.desc)) return out diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index e1de9a6cc..9d3f4c594 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt from qutebrowser.config import config from qutebrowser.utils import utils, usertypes -from qutebrowser.commands import cmdutils +from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils @@ -101,7 +101,7 @@ class KeyHintView(QLabel): def takes_count(cmdstr): """Return true iff this command can take a count argument.""" cmdname = cmdstr.split(' ')[0] - cmd = cmdutils.cmd_dict.get(cmdname) + cmd = objects.commands.get(cmdname) return cmd and cmd.takes_count() bindings_dict = config.key_instance.get_bindings_for(modename) diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index ec558aa37..0bb26954c 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -28,6 +28,7 @@ MYPY = False if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.utils import usertypes + from qutebrowser.commands import command class NoBackend: @@ -39,3 +40,4 @@ class NoBackend: backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend] +commands = {} # type: typing.Dict[str, command.Command] diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index b5c083546..f3217694e 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -30,7 +30,7 @@ import argparse import vulture import qutebrowser.app # pylint: disable=unused-import -from qutebrowser.commands import cmdutils +from qutebrowser.misc import objects from qutebrowser.utils import utils from qutebrowser.browser.webkit import rfc6266 # To run the decorators from there @@ -44,7 +44,7 @@ from qutebrowser.config import configtypes def whitelist_generator(): # noqa """Generator which yields lines to add to a vulture whitelist.""" # qutebrowser commands - for cmd in cmdutils.cmd_dict.values(): + for cmd in objects.commands.values(): yield utils.qualname(cmd.handler) # pyPEG2 classes diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index 8c6f2e44d..ad1397d82 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -35,9 +35,10 @@ 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.commands import cmdutils, argparser +from qutebrowser.commands import argparser from qutebrowser.config import configdata, configtypes from qutebrowser.utils import docutils, usertypes +from qutebrowser.misc import objects from scripts import asciidoc2html, utils FILE_HEADER = """ @@ -350,7 +351,7 @@ def generate_commands(filename): normal_cmds = [] other_cmds = [] debug_cmds = [] - for name, cmd in cmdutils.cmd_dict.items(): + for name, cmd in objects.commands.items(): if cmd.deprecated: continue if usertypes.KeyMode.normal not in cmd.modes: diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index ed6fce36c..4abd58baf 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -326,14 +326,6 @@ class FakeSignal: """ -@attr.s -class FakeCmdUtils: - - """Stub for cmdutils which provides a cmd_dict.""" - - cmd_dict = attr.ib() - - @attr.s(frozen=True) class FakeCommand: diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index 718695874..4d7877e69 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -29,14 +29,14 @@ import enum import pytest -from qutebrowser.commands import cmdutils, cmdexc, argparser, command +from qutebrowser.misc import objects +from qutebrowser.commands import cmdexc, argparser, command, cmdutils from qutebrowser.utils import usertypes @pytest.fixture(autouse=True) def clear_globals(monkeypatch): - """Clear the cmdutils globals between each test.""" - monkeypatch.setattr(cmdutils, 'cmd_dict', {}) + monkeypatch.setattr(objects, 'commands', {}) def _get_cmd(*args, **kwargs): @@ -48,7 +48,7 @@ def _get_cmd(*args, **kwargs): @cmdutils.register(*args, **kwargs) def fun(): """Blah.""" - return cmdutils.cmd_dict['fun'] + return objects.commands['fun'] class TestCheckOverflow: @@ -83,10 +83,10 @@ class TestRegister: def fun(): """Blah.""" - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] assert cmd.handler is fun assert cmd.name == 'fun' - assert len(cmdutils.cmd_dict) == 1 + assert len(objects.commands) == 1 def test_underlines(self): """Make sure the function name is normalized correctly (_ -> -).""" @@ -94,8 +94,8 @@ class TestRegister: def eggs_bacon(): """Blah.""" - assert cmdutils.cmd_dict['eggs-bacon'].name == 'eggs-bacon' - assert 'eggs_bacon' not in cmdutils.cmd_dict + assert objects.commands['eggs-bacon'].name == 'eggs-bacon' + assert 'eggs_bacon' not in objects.commands def test_lowercasing(self): """Make sure the function name is normalized correctly (uppercase).""" @@ -103,8 +103,8 @@ class TestRegister: def Test(): # noqa: N801,N806 pylint: disable=invalid-name """Blah.""" - assert cmdutils.cmd_dict['test'].name == 'test' - assert 'Test' not in cmdutils.cmd_dict + assert objects.commands['test'].name == 'test' + assert 'Test' not in objects.commands def test_explicit_name(self): """Test register with explicit name.""" @@ -112,9 +112,9 @@ class TestRegister: def fun(): """Blah.""" - assert cmdutils.cmd_dict['foobar'].name == 'foobar' - assert 'fun' not in cmdutils.cmd_dict - assert len(cmdutils.cmd_dict) == 1 + assert objects.commands['foobar'].name == 'foobar' + assert 'fun' not in objects.commands + assert len(objects.commands) == 1 def test_multiple_registrations(self): """Make sure registering the same name twice raises ValueError.""" @@ -132,7 +132,7 @@ class TestRegister: @cmdutils.register(instance='foobar') def fun(self): """Blah.""" - assert cmdutils.cmd_dict['fun']._instance == 'foobar' + assert objects.commands['fun']._instance == 'foobar' def test_star_args(self): """Check handling of *args.""" @@ -140,7 +140,7 @@ class TestRegister: def fun(*args): """Blah.""" with pytest.raises(argparser.ArgumentParserError): - cmdutils.cmd_dict['fun'].parser.parse_args([]) + objects.commands['fun'].parser.parse_args([]) def test_star_args_optional(self): """Check handling of *args withstar_args_optional.""" @@ -148,7 +148,7 @@ class TestRegister: def fun(*args): """Blah.""" assert not args - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] cmd.namespace = cmd.parser.parse_args([]) args, kwargs = cmd._get_call_args(win_id=0) fun(*args, **kwargs) @@ -160,7 +160,7 @@ class TestRegister: def fun(arg=False): """Blah.""" assert arg == expected - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] cmd.namespace = cmd.parser.parse_args(inp) assert cmd.namespace.arg == expected @@ -170,7 +170,7 @@ class TestRegister: def fun(arg=False): """Blah.""" assert arg - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] with pytest.raises(argparser.ArgumentParserError): cmd.parser.parse_args(['-a']) @@ -192,14 +192,14 @@ class TestRegister: @cmdutils.argument('win_id', win_id=True) def fun(win_id): """Blah.""" - assert cmdutils.cmd_dict['fun']._get_call_args(42) == ([42], {}) + assert objects.commands['fun']._get_call_args(42) == ([42], {}) def test_count(self): @cmdutils.register() @cmdutils.argument('count', count=True) def fun(count=0): """Blah.""" - assert cmdutils.cmd_dict['fun']._get_call_args(42) == ([0], {}) + assert objects.commands['fun']._get_call_args(42) == ([0], {}) def test_count_without_default(self): with pytest.raises(TypeError, match="fun: handler has count parameter " @@ -216,7 +216,7 @@ class TestRegister: def fun(arg): """Blah.""" - pos_args = cmdutils.cmd_dict['fun'].pos_args + pos_args = objects.commands['fun'].pos_args if hide: assert pos_args == [] else: @@ -251,7 +251,7 @@ class TestRegister: """Blah.""" assert arg == expected - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] cmd.namespace = cmd.parser.parse_args([inp]) if expected is cmdexc.ArgumentTypeError: @@ -270,7 +270,7 @@ class TestRegister: def fun(arg): """Blah.""" - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] cmd.namespace = cmd.parser.parse_args(['fish']) with pytest.raises(cmdexc.ArgumentTypeError): @@ -283,7 +283,7 @@ class TestRegister: def fun(*, arg='foo'): """Blah.""" - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] cmd.namespace = cmd.parser.parse_args(['--arg=fish']) with pytest.raises(cmdexc.ArgumentTypeError): @@ -297,7 +297,7 @@ class TestRegister: def fun(foo, bar, opt=False): """Blah.""" - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b')) assert cmd.get_pos_arg_info(1) == command.ArgInfo(choices=('x', 'y')) with pytest.raises(IndexError): @@ -422,6 +422,6 @@ class TestRun: monkeypatch.setattr(command.objects, 'backend', usertypes.Backend.QtWebKit) - cmd = cmdutils.cmd_dict['fun'] + cmd = objects.commands['fun'] with pytest.raises(cmdexc.PrerequisitesError, match=r'.* backend\.'): cmd.run(win_id=0) diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py index db831bd7e..cd2dea1d4 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_runners.py @@ -21,7 +21,8 @@ import pytest -from qutebrowser.commands import runners, cmdexc, cmdutils +from qutebrowser.misc import objects +from qutebrowser.commands import runners, cmdexc class TestCommandParser: @@ -74,7 +75,7 @@ class TestCompletions: @pytest.fixture(autouse=True) def cmdutils_stub(self, monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" - monkeypatch.setattr(cmdutils, 'cmd_dict', { + monkeypatch.setattr(objects, 'commands', { 'one': stubs.FakeCommand(name='one'), 'two': stubs.FakeCommand(name='two'), 'two-foo': stubs.FakeCommand(name='two-foo'), diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 8620241e4..59291cdd6 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -129,7 +129,7 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch): def config_cycle(option, *values): """For testing varargs.""" - cmd_utils = stubs.FakeCmdUtils({ + commands = { 'set': command.Command(name='set', handler=set_command), 'help': command.Command(name='help', handler=show_help), 'open': command.Command(name='open', handler=openurl, maxsplit=0), @@ -137,8 +137,8 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch): 'tab-give': command.Command(name='tab-give', handler=tab_give), 'config-cycle': command.Command(name='config-cycle', handler=config_cycle), - }) - monkeypatch.setattr(completer, 'cmdutils', cmd_utils) + } + monkeypatch.setattr(completer.objects, 'commands', commands) def _set_cmd_prompt(cmd, txt): diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 28265689c..9e75daae8 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -27,11 +27,11 @@ from datetime import datetime import pytest from PyQt5.QtCore import QUrl +from qutebrowser.misc import objects from qutebrowser.completion import completer from qutebrowser.completion.models import miscmodels, urlmodel, configmodel from qutebrowser.config import configdata, configtypes from qutebrowser.utils import usertypes -from qutebrowser.commands import cmdutils def _check_completions(model, expected): @@ -66,7 +66,7 @@ def _check_completions(model, expected): @pytest.fixture() def cmdutils_stub(monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" - return monkeypatch.setattr(cmdutils, 'cmd_dict', { + return monkeypatch.setattr(objects, 'commands', { 'quit': stubs.FakeCommand(name='quit', desc='quit qutebrowser'), 'open': stubs.FakeCommand(name='open', desc='open a url'), 'prompt-yes': stubs.FakeCommand(name='prompt-yes', deprecated=True), diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index b2775de2f..a9a61e952 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -33,6 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QFont from PyQt5.QtNetwork import QNetworkProxy +from qutebrowser.misc import objects from qutebrowser.config import configtypes, configexc, configutils from qutebrowser.utils import debug, utils, qtutils, urlmatch from qutebrowser.browser.network import pac @@ -1208,11 +1209,11 @@ class TestCommand: @pytest.fixture def patch_cmdutils(self, monkeypatch, stubs): """Patch the cmdutils module to provide fake commands.""" - cmd_utils = stubs.FakeCmdUtils({ + commands = { 'cmd1': stubs.FakeCommand(desc="desc 1"), - 'cmd2': stubs.FakeCommand(desc="desc 2")}) - monkeypatch.setattr(configtypes, 'cmdutils', cmd_utils) - monkeypatch.setattr('qutebrowser.commands.runners.cmdutils', cmd_utils) + 'cmd2': stubs.FakeCommand(desc="desc 2"), + } + monkeypatch.setattr(objects, 'commands', commands) @pytest.fixture def klass(self): diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 7c9727b65..9af30fd16 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -21,6 +21,7 @@ import pytest +from qutebrowser.misc import objects from qutebrowser.misc.keyhintwidget import KeyHintView @@ -120,7 +121,7 @@ def test_suggestions_special(keyhint, config_stub): def test_suggestions_with_count(keyhint, config_stub, monkeypatch, stubs): """Test that a count prefix filters out commands that take no count.""" - monkeypatch.setattr('qutebrowser.commands.cmdutils.cmd_dict', { + monkeypatch.setattr(objects, 'commands', { 'foo': stubs.FakeCommand(name='foo', takes_count=lambda: False), 'bar': stubs.FakeCommand(name='bar', takes_count=lambda: True), }) From 7cbba4b3f19aa2a33df73103b45c9e898ac8badb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 13:29:33 +0100 Subject: [PATCH 093/258] Add type annotations for cmdutils --- mypy.ini | 4 ++++ qutebrowser/commands/cmdutils.py | 31 +++++++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/mypy.ini b/mypy.ini index 84a6fb239..288cc6515 100644 --- a/mypy.ini +++ b/mypy.ini @@ -57,3 +57,7 @@ disallow_incomplete_defs = True [mypy-qutebrowser.misc.objects] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.commands.cmdutils] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 12ffabe3a..b9874c85b 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -20,14 +20,14 @@ """Contains various command utils and a global command dict.""" import inspect -import typing # pylint: disable=unused-import +import typing from qutebrowser.misc import objects from qutebrowser.utils import qtutils, log from qutebrowser.commands import command, cmdexc -def check_overflow(arg, ctype): +def check_overflow(arg: int, ctype: str) -> None: """Check if the given argument is in bounds for the given type. Args: @@ -42,7 +42,8 @@ def check_overflow(arg, ctype): "representation.".format(ctype)) -def check_exclusive(flags, names): +def check_exclusive(flags: typing.Iterable[bool], + names: typing.Iterable[str]) -> None: """Check if only one flag is set with exclusive flags. Raise a CommandError if not. @@ -70,7 +71,10 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name _kwargs: The arguments to pass to Command. """ - def __init__(self, *, instance=None, name=None, **kwargs): + def __init__(self, *, + instance: str = None, + name: str = None, + **kwargs: typing.Any) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -82,7 +86,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name self._name = name self._kwargs = kwargs - def __call__(self, func): + def __call__(self, func: typing.Callable) -> typing.Callable: """Register the command before running the function. Gets called when a function should be decorated. @@ -101,8 +105,9 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name else: assert isinstance(self._name, str), self._name name = self._name - log.commands.vdebug("Registering command {} (from {}:{})".format( - name, func.__module__, func.__qualname__)) + log.commands.vdebug( # type: ignore + "Registering command {} (from {}:{})" + .format(name, func.__module__, func.__qualname__)) if name in objects.commands: raise ValueError("{} is already registered!".format(name)) cmd = command.Command(name=name, instance=self._instance, @@ -123,21 +128,23 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name _kwargs: Keyword arguments, valid ArgInfo members """ - def __init__(self, argname, **kwargs): + def __init__(self, argname: str, **kwargs: typing.Any) -> None: self._argname = argname self._kwargs = kwargs - def __call__(self, func): + def __call__(self, func: typing.Callable) -> typing.Callable: funcname = func.__name__ if self._argname not in inspect.signature(func).parameters: raise ValueError("{} has no argument {}!".format(funcname, self._argname)) if not hasattr(func, 'qute_args'): - func.qute_args = {} - elif func.qute_args is None: + func.qute_args = {} # type: ignore + elif func.qute_args is None: # type: ignore raise ValueError("@cmdutils.argument got called above (after) " "@cmdutils.register for {}!".format(funcname)) - func.qute_args[self._argname] = command.ArgInfo(**self._kwargs) + arginfo = command.ArgInfo(**self._kwargs) + func.qute_args[self._argname] = arginfo # type: ignore + return func From e576847b911e334c641757ff7cc87e0fb3f70a1b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 13:29:40 +0100 Subject: [PATCH 094/258] Add partial type annotations for qtutils --- qutebrowser/utils/qtutils.py | 68 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index d2b7f2ffc..5373e76aa 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -31,11 +31,12 @@ Module attributes: import io import operator import contextlib +import typing # pylint: disable=unused-import,useless-suppression import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QSaveFile, QT_VERSION_STR, - PYQT_VERSION_STR) + PYQT_VERSION_STR, QFileDevice, QObject) try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover @@ -61,19 +62,22 @@ class QtOSError(OSError): qt_errno: The error attribute of the given QFileDevice, if applicable. """ - def __init__(self, dev, msg=None): + def __init__(self, dev: QFileDevice, msg: str = None) -> None: if msg is None: msg = dev.errorString() super().__init__(msg) + self.qt_errno = None # type: typing.Optional[QFileDevice.FileError] try: self.qt_errno = dev.error() except AttributeError: - self.qt_errno = None + pass -def version_check(version, exact=False, compiled=True): +def version_check(version: str, + exact: bool = False, + compiled: bool = True) -> bool: """Check if the Qt runtime version is the version supplied or newer. Args: @@ -103,14 +107,14 @@ def version_check(version, exact=False, compiled=True): MAX_WORLD_ID = 256 if version_check('5.11.2') else 11 -def is_new_qtwebkit(): +def is_new_qtwebkit() -> bool: """Check if the given version is a new QtWebKit.""" assert qWebKitVersion is not None return (pkg_resources.parse_version(qWebKitVersion()) > pkg_resources.parse_version('538.1')) -def check_overflow(arg, ctype, fatal=True): +def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int: """Check if the given argument is in bounds for the given type. Args: @@ -138,13 +142,13 @@ def check_overflow(arg, ctype, fatal=True): return arg -def ensure_valid(obj): +def ensure_valid(obj: QObject) -> None: """Ensure a Qt object with an .isValid() method is valid.""" if not obj.isValid(): raise QtValueError(obj) -def check_qdatastream(stream): +def check_qdatastream(stream: QDataStream) -> None: """Check the status of a QDataStream and raise OSError if it's not ok.""" status_to_str = { QDataStream.Ok: "The data stream is operating normally.", @@ -158,7 +162,7 @@ def check_qdatastream(stream): raise OSError(status_to_str[stream.status()]) -def serialize(obj): +def serialize(obj: QObject) -> QByteArray: """Serialize an object into a QByteArray.""" data = QByteArray() stream = QDataStream(data, QIODevice.WriteOnly) @@ -166,20 +170,20 @@ def serialize(obj): return data -def deserialize(data, obj): +def deserialize(data: QByteArray, obj: QObject) -> None: """Deserialize an object from a QByteArray.""" stream = QDataStream(data, QIODevice.ReadOnly) deserialize_stream(stream, obj) -def serialize_stream(stream, obj): +def serialize_stream(stream: QDataStream, obj: QObject) -> None: """Serialize an object into a QDataStream.""" check_qdatastream(stream) stream << obj # pylint: disable=pointless-statement check_qdatastream(stream) -def deserialize_stream(stream, obj): +def deserialize_stream(stream: QDataStream, obj: QObject) -> None: """Deserialize a QDataStream into an object.""" check_qdatastream(stream) stream >> obj # pylint: disable=pointless-statement @@ -195,11 +199,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'): open_ok = f.open(QIODevice.WriteOnly) if not open_ok: raise QtOSError(f) + if binary: new_f = PyQIODevice(f) else: new_f = io.TextIOWrapper(PyQIODevice(f), encoding=encoding) + yield new_f + new_f.flush() except: f.cancelWriting() @@ -219,29 +226,29 @@ class PyQIODevice(io.BufferedIOBase): dev: The underlying QIODevice. """ - def __init__(self, dev): + def __init__(self, dev: QIODevice) -> None: super().__init__() self.dev = dev - def __len__(self): + def __len__(self) -> int: return self.dev.size() - def _check_open(self): + def _check_open(self) -> None: """Check if the device is open, raise ValueError if not.""" if not self.dev.isOpen(): raise ValueError("IO operation on closed device!") - def _check_random(self): + def _check_random(self) -> None: """Check if the device supports random access, raise OSError if not.""" if not self.seekable(): raise OSError("Random access not allowed!") - def _check_readable(self): + def _check_readable(self) -> None: """Check if the device is readable, raise OSError if not.""" if not self.dev.isReadable(): raise OSError("Trying to read unreadable file!") - def _check_writable(self): + def _check_writable(self) -> None: """Check if the device is writable, raise OSError if not.""" if not self.writable(): raise OSError("Trying to write to unwritable file!") @@ -263,7 +270,7 @@ class PyQIODevice(io.BufferedIOBase): raise QtOSError(self.dev) return contextlib.closing(self) - def close(self): + def close(self) -> None: """Close the underlying device.""" self.dev.close() @@ -289,18 +296,18 @@ class PyQIODevice(io.BufferedIOBase): raise io.UnsupportedOperation @property - def closed(self): + def closed(self) -> bool: return not self.dev.isOpen() - def flush(self): + def flush(self) -> None: self._check_open() self.dev.waitForBytesWritten(-1) - def isatty(self): + def isatty(self) -> bool: self._check_open() return False - def readable(self): + def readable(self) -> bool: return self.dev.isReadable() def readline(self, size=-1): @@ -326,18 +333,18 @@ class PyQIODevice(io.BufferedIOBase): raise QtOSError(self.dev) return buf - def seekable(self): + def seekable(self) -> bool: return not self.dev.isSequential() - def tell(self): + def tell(self) -> int: self._check_open() self._check_random() return self.dev.pos() - def writable(self): + def writable(self) -> bool: return self.dev.isWritable() - def write(self, b): + def write(self, b: bytes) -> int: self._check_open() self._check_writable() num = self.dev.write(b) @@ -361,7 +368,7 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj): + def __init__(self, obj: QObject) -> None: try: self.reason = obj.errorString() except AttributeError: @@ -379,7 +386,7 @@ class EventLoop(QEventLoop): Raises an exception when doing exec_() multiple times. """ - def __init__(self, parent=None): + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._executing = False @@ -388,5 +395,6 @@ class EventLoop(QEventLoop): if self._executing: raise AssertionError("Eventloop is already running!") self._executing = True - super().exec_(flags) + status = super().exec_(flags) self._executing = False + return status From f9858733c1ba7d4500ad8b20bef281341dc12f12 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 13:32:20 +0100 Subject: [PATCH 095/258] Improve docstrings --- qutebrowser/commands/cmdutils.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index b9874c85b..a30b7e799 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Contains various command utils and a global command dict.""" +"""Utilities for command handlers.""" import inspect import typing @@ -31,8 +31,8 @@ def check_overflow(arg: int, ctype: str) -> None: """Check if the given argument is in bounds for the given type. Args: - arg: The argument to check - ctype: The C/Qt type to check as a string. + arg: The argument to check. + ctype: The C++/Qt type to check as a string ('int'/'int64'). """ try: qtutils.check_overflow(arg, ctype) @@ -49,8 +49,8 @@ def check_exclusive(flags: typing.Iterable[bool], Raise a CommandError if not. Args: - flags: An iterable of booleans to check. - names: An iterable of flag names for the error message. + flags: The flag values to check. + names: A list of names (corresponding to the flags argument). """ if sum(1 for e in flags if e) > 1: argstr = '/'.join('-' + e for e in names) @@ -62,9 +62,6 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to register a new command handler. - This could also be a function, but as a class (with a "wrong" name) it's - much cleaner to implement. - 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. @@ -120,9 +117,6 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to customize an argument for @cmdutils.register. - This could also be a function, but as a class (with a "wrong" name) it's - much cleaner to implement. - Attributes: _argname: The name of the argument to handle. _kwargs: Keyword arguments, valid ArgInfo members From b7de287e7bf053072eab47551e593d948fa8ced7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 14:09:06 +0100 Subject: [PATCH 096/258] Move CommandError to api.cmdutils --- doc/contributing.asciidoc | 4 +- qutebrowser/api/__init__.py | 26 ++++ qutebrowser/{commands => api}/cmdutils.py | 25 +++- qutebrowser/app.py | 10 +- qutebrowser/browser/adblock.py | 2 +- qutebrowser/browser/commands.py | 141 +++++++++--------- qutebrowser/browser/downloads.py | 26 ++-- qutebrowser/browser/greasemonkey.py | 2 +- qutebrowser/browser/hints.py | 25 ++-- qutebrowser/browser/history.py | 5 +- qutebrowser/browser/urlmarks.py | 4 +- qutebrowser/commands/cmdexc.py | 5 - qutebrowser/commands/runners.py | 5 +- qutebrowser/completion/completionwidget.py | 10 +- .../completion/models/completionmodel.py | 4 +- qutebrowser/config/configcommands.py | 58 +++---- qutebrowser/keyinput/macros.py | 7 +- qutebrowser/keyinput/modeman.py | 6 +- qutebrowser/mainwindow/mainwindow.py | 3 +- qutebrowser/mainwindow/prompt.py | 4 +- qutebrowser/mainwindow/statusbar/command.py | 6 +- qutebrowser/misc/crashsignal.py | 2 +- qutebrowser/misc/readline.py | 2 +- qutebrowser/misc/savemanager.py | 2 +- qutebrowser/misc/sessions.py | 40 ++--- qutebrowser/misc/utilcmds.py | 33 ++-- qutebrowser/utils/urlutils.py | 4 +- scripts/dev/check_coverage.py | 4 +- tests/unit/{commands => api}/test_cmdutils.py | 9 +- tests/unit/browser/test_history.py | 4 +- tests/unit/completion/test_completer.py | 3 +- tests/unit/completion/test_completionmodel.py | 4 +- .../unit/completion/test_completionwidget.py | 4 +- tests/unit/config/test_configcommands.py | 50 +++---- tests/unit/misc/test_utilcmds.py | 8 +- tests/unit/utils/test_urlutils.py | 4 +- 36 files changed, 302 insertions(+), 249 deletions(-) create mode 100644 qutebrowser/api/__init__.py rename qutebrowser/{commands => api}/cmdutils.py (86%) rename tests/unit/{commands => api}/test_cmdutils.py (98%) diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 1a2369516..5f0e9bf9c 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -407,7 +407,7 @@ Creating a new command is straightforward: [source,python] ---- -import qutebrowser.commands.cmdutils +from qutebrowser.api import cmdutils ... @@ -429,7 +429,7 @@ selects which object registry (global, per-tab, etc.) to use. See the There are also other arguments to customize the way the command is registered; see the class documentation for `register` in -`qutebrowser.commands.cmdutils` for details. +`qutebrowser.api.cmdutils` for details. 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 diff --git a/qutebrowser/api/__init__.py b/qutebrowser/api/__init__.py new file mode 100644 index 000000000..648887005 --- /dev/null +++ b/qutebrowser/api/__init__.py @@ -0,0 +1,26 @@ +# 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 . + +"""API for extensions. + +This API currently isn't exposed to third-party extensions yet, but will be in +the future. Thus, care must be taken when adding new APIs here. + +Code in qutebrowser.components only uses this API. +""" diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/api/cmdutils.py similarity index 86% rename from qutebrowser/commands/cmdutils.py rename to qutebrowser/api/cmdutils.py index a30b7e799..a90471a35 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -27,6 +27,23 @@ from qutebrowser.utils import qtutils, log from qutebrowser.commands import command, cmdexc +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: + + 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. + """ + + def check_overflow(arg: int, ctype: str) -> None: """Check if the given argument is in bounds for the given type. @@ -37,9 +54,8 @@ def check_overflow(arg: int, ctype: str) -> None: try: qtutils.check_overflow(arg, ctype) except OverflowError: - raise cmdexc.CommandError( - "Numeric argument is too large for internal {} " - "representation.".format(ctype)) + raise CommandError("Numeric argument is too large for internal {} " + "representation.".format(ctype)) def check_exclusive(flags: typing.Iterable[bool], @@ -54,8 +70,7 @@ def check_exclusive(flags: typing.Iterable[bool], """ if sum(1 for e in flags if e) > 1: argstr = '/'.join('-' + e for e in names) - raise cmdexc.CommandError("Only one of {} can be given!".format( - argstr)) + raise CommandError("Only one of {} can be given!".format(argstr)) class register: # noqa: N801,N806 pylint: disable=invalid-name diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 161bc5297..27848c4c1 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -60,7 +60,8 @@ except ImportError: import qutebrowser import qutebrowser.resources from qutebrowser.completion.models import miscmodels -from qutebrowser.commands import cmdutils, runners, cmdexc +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, qtnetworkdownloads, downloads, greasemonkey) @@ -619,10 +620,11 @@ class Quitter: ok = self.restart(session='_restart') except sessions.SessionError as e: log.destroy.exception("Failed to save session!") - raise cmdexc.CommandError("Failed to save session: {}!".format(e)) + raise cmdutils.CommandError("Failed to save session: {}!" + .format(e)) except SyntaxError as e: log.destroy.exception("Got SyntaxError") - raise cmdexc.CommandError("SyntaxError in {}:{}: {}".format( + raise cmdutils.CommandError("SyntaxError in {}:{}: {}".format( e.filename, e.lineno, e)) if ok: self.shutdown(restart=True) @@ -684,7 +686,7 @@ class Quitter: session: The name of the session to save. """ if session is not None and not save: - raise cmdexc.CommandError("Session name given without --save!") + raise cmdutils.CommandError("Session name given without --save!") if save: if session is None: session = sessions.default diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index d2a21639c..fdec79d0f 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -28,7 +28,7 @@ import zipfile from qutebrowser.browser import downloads from qutebrowser.config import config from qutebrowser.utils import objreg, standarddir, log, message -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils def _guess_zip_filename(zf): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e9d6b8b68..df6906515 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -29,7 +29,8 @@ from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtPrintSupport import QPrintPreviewDialog -from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners +from qutebrowser.commands import userscripts, runners +from qutebrowser.api import cmdutils from qutebrowser.config import config, configdata from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) @@ -68,8 +69,8 @@ class CommandDispatcher: """Get a tabbed-browser from a new window.""" args = QApplication.instance().arguments() if private and '--single-process' in args: - raise cmdexc.CommandError("Private windows are unavailable with " - "the single-process process model.") + raise cmdutils.CommandError("Private windows are unavailable with " + "the single-process process model.") new_window = mainwindow.MainWindow(private=private) new_window.show() @@ -97,7 +98,7 @@ class CommandDispatcher: if e.reason: msg += " ({})".format(e.reason) msg += "!" - raise cmdexc.CommandError(msg) + raise cmdutils.CommandError(msg) def _current_title(self): """Convenience method to get the current title.""" @@ -107,7 +108,7 @@ class CommandDispatcher: """Get the currently active widget from a command.""" widget = self._tabbed_browser.widget.currentWidget() if widget is None: - raise cmdexc.CommandError("No WebView available yet!") + raise cmdutils.CommandError("No WebView available yet!") return widget def _open(self, url, tab=False, background=False, window=False, @@ -166,10 +167,10 @@ class CommandDispatcher: except KeyError: if not show_error: return - raise cmdexc.CommandError("No last focused tab!") + raise cmdutils.CommandError("No last focused tab!") idx = self._tabbed_browser.widget.indexOf(tab) if idx == -1: - raise cmdexc.CommandError("Last focused tab vanished!") + raise cmdutils.CommandError("Last focused tab vanished!") self._set_current_index(idx) def _get_selection_override(self, prev, next_, opposite): @@ -197,7 +198,7 @@ class CommandDispatcher: elif conf_selection == QTabBar.SelectRightTab: return QTabBar.SelectLeftTab elif conf_selection == QTabBar.SelectPreviousTab: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "-o is not supported with 'tabs.select_on_remove' set to " "'last-used'!") else: # pragma: no cover @@ -339,7 +340,7 @@ class CommandDispatcher: try: return urlutils.fuzzy_url(url, force_search=force_search) except urlutils.InvalidUrlError as e: - # We don't use cmdexc.CommandError here as this can be + # We don't use cmdutils.CommandError here as this can be # called async from edit_url message.error(str(e)) return None @@ -444,7 +445,7 @@ class CommandDispatcher: else: tab.printing.show_dialog() except browsertab.WebTabError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_clone(self, bg=False, window=False): @@ -464,7 +465,7 @@ class CommandDispatcher: try: history = curtab.history.serialize() except browsertab.WebTabError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) # The new tab could be in a new tabbed_browser (e.g. because of # tabs.tabs_are_windows being set) @@ -504,7 +505,8 @@ class CommandDispatcher: tabbed_browser, tab = self._resolve_buffer_index(index) if tabbed_browser is self._tabbed_browser: - raise cmdexc.CommandError("Can't take a tab from the same window") + raise cmdutils.CommandError("Can't take a tab from the same " + "window") self._open(tab.url(), tab=True) if not keep: @@ -528,18 +530,18 @@ class CommandDispatcher: win_id = count - 1 if win_id == self._win_id: - raise cmdexc.CommandError("Can't give a tab to the same window") + raise cmdutils.CommandError("Can't give a tab to the same window") if win_id is None: if self._count() < 2 and not keep: - raise cmdexc.CommandError("Cannot detach from a window with " - "only one tab") + raise cmdutils.CommandError("Cannot detach from a window with " + "only one tab") tabbed_browser = self._new_tabbed_browser( private=self._tabbed_browser.is_private) else: if win_id not in objreg.window_registry: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "There's no window with id {}!".format(win_id)) tabbed_browser = objreg.get('tabbed-browser', scope='window', @@ -555,9 +557,9 @@ class CommandDispatcher: history = self._current_widget().history # Catch common cases before e.g. cloning tab if not forward and not history.can_go_back(): - raise cmdexc.CommandError("At beginning of history.") + raise cmdutils.CommandError("At beginning of history.") elif forward and not history.can_go_forward(): - raise cmdexc.CommandError("At end of history.") + raise cmdutils.CommandError("At end of history.") if tab or bg or window: widget = self.tab_clone(bg, window) @@ -570,7 +572,7 @@ class CommandDispatcher: else: widget.history.back(count) except browsertab.WebTabError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -663,7 +665,7 @@ class CommandDispatcher: raise ValueError("Got called with invalid value {} for " "`where'.".format(where)) except navigate.Error as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -709,9 +711,9 @@ class CommandDispatcher: func = funcs[direction] except KeyError: expected_values = ', '.join(sorted(funcs)) - raise cmdexc.CommandError("Invalid value {!r} for direction - " - "expected one of: {}".format( - direction, expected_values)) + raise cmdutils.CommandError("Invalid value {!r} for direction - " + "expected one of: {}".format( + direction, expected_values)) if direction in ['top', 'bottom']: func() @@ -794,7 +796,7 @@ class CommandDispatcher: try: tab.scroller.delta_page(count * x, count * y) except OverflowError: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "Numeric argument is too large for internal int " "representation.") @@ -901,7 +903,7 @@ class CommandDispatcher: try: perc = tab.zoom.offset(count) except ValueError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) if not quiet: message.info("Zoom level: {}%".format(int(perc)), replace=True) @@ -918,7 +920,7 @@ class CommandDispatcher: try: perc = tab.zoom.offset(-count) except ValueError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) if not quiet: message.info("Zoom level: {}%".format(int(perc)), replace=True) @@ -940,8 +942,8 @@ class CommandDispatcher: try: zoom = int(zoom.rstrip('%')) except ValueError: - raise cmdexc.CommandError("zoom: Invalid int value {}" - .format(zoom)) + raise cmdutils.CommandError("zoom: Invalid int value {}" + .format(zoom)) level = count if count is not None else zoom if level is None: @@ -951,7 +953,7 @@ class CommandDispatcher: try: tab.zoom.set_factor(float(level) / 100) except ValueError: - raise cmdexc.CommandError("Can't zoom {}%!".format(level)) + raise cmdutils.CommandError("Can't zoom {}%!".format(level)) if not quiet: message.info("Zoom level: {}%".format(int(level)), replace=True) @@ -1000,7 +1002,7 @@ class CommandDispatcher: try: self._tabbed_browser.undo() except IndexError: - raise cmdexc.CommandError("Nothing to undo!") + raise cmdutils.CommandError("Nothing to undo!") @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) @@ -1061,7 +1063,7 @@ class CommandDispatcher: index = model.data(model.first_item()) index_parts = index.split('/', 1) else: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "No matching tab for: {}".format(index)) if len(index_parts) == 2: @@ -1072,18 +1074,18 @@ class CommandDispatcher: active_win = objreg.get('app').activeWindow() if active_win is None: # Not sure how you enter a command without an active window... - raise cmdexc.CommandError( + raise cmdutils.CommandError( "No window specified and couldn't find active window!") win_id = active_win.win_id if win_id not in objreg.window_registry: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "There's no window with id {}!".format(win_id)) tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) if not 0 < idx <= tabbed_browser.widget.count(): - raise cmdexc.CommandError( + raise cmdutils.CommandError( "There's no tab with index {}!".format(idx)) return (tabbed_browser, tabbed_browser.widget.widget(idx-1)) @@ -1158,7 +1160,7 @@ class CommandDispatcher: if 1 <= index <= self._count(): self._set_current_index(index - 1) else: - raise cmdexc.CommandError("There's no tab with index {}!".format( + raise cmdutils.CommandError("There's no tab with index {}!".format( index)) @cmdutils.register(instance='command-dispatcher', scope='window') @@ -1200,8 +1202,8 @@ class CommandDispatcher: new_idx = 0 if not 0 <= new_idx < self._count(): - raise cmdexc.CommandError("Can't move tab to position {}!".format( - new_idx + 1)) + raise cmdutils.CommandError("Can't move tab to position {}!" + .format(new_idx + 1)) cur_idx = self._current_index() cmdutils.check_overflow(cur_idx, 'int') @@ -1232,8 +1234,8 @@ class CommandDispatcher: try: cmd, *args = shlex.split(cmdline) except ValueError as e: - raise cmdexc.CommandError("Error while splitting command: " - "{}".format(e)) + raise cmdutils.CommandError("Error while splitting command: " + "{}".format(e)) args = runners.replace_variables(self._win_id, args) @@ -1252,7 +1254,7 @@ class CommandDispatcher: try: runner = self._run_userscript(s, cmd, args, verbose, count) runner.finished.connect(_on_proc_finished) - except cmdexc.CommandError as e: + except cmdutils.CommandError as e: message.error(str(e)) # ~ expansion is handled by the userscript module. @@ -1312,7 +1314,7 @@ class CommandDispatcher: runner = userscripts.run_async( tab, cmd, *args, win_id=self._win_id, env=env, verbose=verbose) except userscripts.Error as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) return runner @cmdutils.register(instance='command-dispatcher', scope='window') @@ -1336,7 +1338,7 @@ class CommandDispatcher: try: url = objreg.get('quickmark-manager').get(name) except urlmarks.Error as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window', @@ -1356,11 +1358,12 @@ class CommandDispatcher: try: name = quickmark_manager.get_by_qurl(url) except urlmarks.DoesNotExistError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) try: quickmark_manager.delete(name) except KeyError: - raise cmdexc.CommandError("Quickmark '{}' not found!".format(name)) + raise cmdutils.CommandError("Quickmark '{}' not found!" + .format(name)) @cmdutils.register(instance='command-dispatcher', scope='window') def bookmark_add(self, url=None, title=None, toggle=False): @@ -1382,8 +1385,8 @@ class CommandDispatcher: already exists. """ if url and not title: - raise cmdexc.CommandError('Title must be provided if url has ' - 'been provided') + raise cmdutils.CommandError('Title must be provided if url has ' + 'been provided') bookmark_manager = objreg.get('bookmark-manager') if not url: url = self._current_url() @@ -1391,13 +1394,13 @@ class CommandDispatcher: try: url = urlutils.fuzzy_url(url) except urlutils.InvalidUrlError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) if not title: title = self._current_title() try: was_added = bookmark_manager.add(url, title, toggle=toggle) except urlmarks.Error as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) else: msg = "Bookmarked {}" if was_added else "Removed bookmark {}" message.info(msg.format(url.toDisplayString())) @@ -1419,7 +1422,7 @@ class CommandDispatcher: try: qurl = urlutils.fuzzy_url(url) except urlutils.InvalidUrlError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) self._open(qurl, tab, bg, window) if delete: self.bookmark_del(url) @@ -1440,7 +1443,7 @@ class CommandDispatcher: try: objreg.get('bookmark-manager').delete(url) except KeyError: - raise cmdexc.CommandError("Bookmark '{}' not found!".format(url)) + raise cmdutils.CommandError("Bookmark '{}' not found!".format(url)) @cmdutils.register(instance='command-dispatcher', scope='window') def follow_selected(self, *, tab=False): @@ -1452,7 +1455,7 @@ class CommandDispatcher: try: self._current_widget().caret.follow_selected(tab=tab) except browsertab.WebTabError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') @@ -1474,7 +1477,7 @@ class CommandDispatcher: else: tab.data.inspector.toggle(page) except inspector.WebInspectorError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') def download(self, url=None, *, mhtml_=False, dest=None): @@ -1491,7 +1494,7 @@ class CommandDispatcher: if dest is not None: dest = downloads.transform_path(dest) if dest is None: - raise cmdexc.CommandError("Invalid target filename") + raise cmdutils.CommandError("Invalid target filename") target = downloads.FileDownloadTarget(dest) tab = self._current_widget() @@ -1499,8 +1502,8 @@ class CommandDispatcher: if url: if mhtml_: - raise cmdexc.CommandError("Can only download the current page" - " as mhtml.") + raise cmdutils.CommandError("Can only download the current " + "page as mhtml.") url = urlutils.qurl_from_user_input(url) urlutils.raise_cmdexc_if_invalid(url) download_manager.get(url, user_agent=user_agent, target=target) @@ -1512,7 +1515,7 @@ class CommandDispatcher: try: webengine_download_manager.get_mhtml(tab, target) except browsertab.UnsupportedOperationError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) else: download_manager.get_mhtml(tab, target) else: @@ -1544,12 +1547,12 @@ class CommandDispatcher: tab = self._current_widget() try: current_url = self._current_url() - except cmdexc.CommandError as e: + except cmdutils.CommandError as e: message.error(str(e)) return if current_url.scheme() == 'view-source' or tab.data.viewing_source: - raise cmdexc.CommandError("Already viewing source!") + raise cmdutils.CommandError("Already viewing source!") if edit: ed = editor.ExternalEditor(self._tabbed_browser) @@ -1613,13 +1616,13 @@ class CommandDispatcher: elif topic.startswith(':'): command = topic[1:] if command not in objects.commands: - raise cmdexc.CommandError("Invalid command {}!".format( + raise cmdutils.CommandError("Invalid command {}!".format( command)) path = 'commands.html#{}'.format(command) elif topic in configdata.DATA: path = 'settings.html#{}'.format(topic) else: - raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) + raise cmdutils.CommandError("Invalid help topic {}!".format(topic)) url = QUrl('qute://help/{}'.format(path)) self._open(url, tab, bg, window) @@ -1637,7 +1640,7 @@ class CommandDispatcher: window: Open in a new window. """ if level.upper() not in log.LOG_LEVELS: - raise cmdexc.CommandError("Invalid log level {}!".format(level)) + raise cmdutils.CommandError("Invalid log level {}!".format(level)) if plain: url = QUrl('qute://plainlog?level={}'.format(level)) else: @@ -1839,7 +1842,7 @@ class CommandDispatcher: window_options = self._tabbed_browser.search_options if window_text is None: - raise cmdexc.CommandError("No search done yet.") + raise cmdutils.CommandError("No search done yet.") self.set_mark("'") @@ -1873,7 +1876,7 @@ class CommandDispatcher: window_options = self._tabbed_browser.search_options if window_text is None: - raise cmdexc.CommandError("No search done yet.") + raise cmdutils.CommandError("No search done yet.") self.set_mark("'") @@ -2070,7 +2073,7 @@ class CommandDispatcher: try: tab.action.run_string(action) except browsertab.WebTabError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) @@ -2121,13 +2124,13 @@ class CommandDispatcher: with open(path, 'r', encoding='utf-8') as f: js_code = f.read() except OSError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) widget = self._current_widget() try: widget.run_js_async(js_code, callback=jseval_cb, world=world) except browsertab.WebTabError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) @cmdutils.register(instance='command-dispatcher', scope='window') def fake_key(self, keystring, global_=False): @@ -2144,7 +2147,7 @@ class CommandDispatcher: try: sequence = keyutils.KeySequence.parse(keystring) except keyutils.KeyParseError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) for keyinfo in sequence: press_event = keyinfo.to_event(QEvent.KeyPress) @@ -2153,7 +2156,7 @@ class CommandDispatcher: if global_: window = QApplication.focusWindow() if window is None: - raise cmdexc.CommandError("No focused window!") + raise cmdutils.CommandError("No focused window!") QApplication.postEvent(window, press_event) QApplication.postEvent(window, release_event) else: @@ -2266,4 +2269,4 @@ class CommandDispatcher: try: tab.audio.toggle_muted(override=True) except browsertab.WebTabError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index c640396f6..6d9fa6c4e 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, QTimer, QAbstractListModel, QUrl) from qutebrowser.browser import pdfjs -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils from qutebrowser.config import config from qutebrowser.utils import (usertypes, standarddir, utils, message, log, qtutils, objreg) @@ -1009,8 +1009,8 @@ class DownloadModel(QAbstractListModel): count: The index of the download """ if not count: - raise cmdexc.CommandError("There's no download!") - raise cmdexc.CommandError("There's no download {}!".format(count)) + raise cmdutils.CommandError("There's no download!") + raise cmdutils.CommandError("There's no download {}!".format(count)) @cmdutils.register(instance='download-model', scope='window') @cmdutils.argument('count', count=True) @@ -1034,8 +1034,8 @@ class DownloadModel(QAbstractListModel): if download.done: if not count: count = len(self) - raise cmdexc.CommandError("Download {} is already done!" - .format(count)) + raise cmdutils.CommandError("Download {} is already done!" + .format(count)) download.cancel() @cmdutils.register(instance='download-model', scope='window') @@ -1053,7 +1053,8 @@ class DownloadModel(QAbstractListModel): if not download.successful: if not count: count = len(self) - raise cmdexc.CommandError("Download {} is not done!".format(count)) + raise cmdutils.CommandError("Download {} is not done!" + .format(count)) download.delete() download.remove() log.downloads.debug("deleted download {}".format(download)) @@ -1080,7 +1081,8 @@ class DownloadModel(QAbstractListModel): if not download.successful: if not count: count = len(self) - raise cmdexc.CommandError("Download {} is not done!".format(count)) + raise cmdutils.CommandError("Download {} is not done!" + .format(count)) download.open_file(cmdline) @cmdutils.register(instance='download-model', scope='window') @@ -1097,12 +1099,12 @@ class DownloadModel(QAbstractListModel): except IndexError: self._raise_no_download(count) if download.successful or not download.done: - raise cmdexc.CommandError("Download {} did not fail!".format( - count)) + raise cmdutils.CommandError("Download {} did not fail!" + .format(count)) else: to_retry = [d for d in self if d.done and not d.successful] if not to_retry: - raise cmdexc.CommandError("No failed downloads!") + raise cmdutils.CommandError("No failed downloads!") else: download = to_retry[0] download.try_retry() @@ -1137,8 +1139,8 @@ class DownloadModel(QAbstractListModel): if not download.done: if not count: count = len(self) - raise cmdexc.CommandError("Download {} is not done!" - .format(count)) + raise cmdutils.CommandError("Download {} is not done!" + .format(count)) download.remove() def running_downloads(self): diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index a2ae73aab..9314f81c6 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import pyqtSignal, QObject, QUrl from qutebrowser.utils import (log, standarddir, jinja, objreg, utils, javascript, urlmatch, version, usertypes) -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils from qutebrowser.browser import downloads from qutebrowser.misc import objects diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 730128295..230437b40 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -34,7 +34,8 @@ from PyQt5.QtWidgets import QLabel from qutebrowser.config import config, configexc from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.browser import webelem -from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners +from qutebrowser.commands import userscripts, runners +from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils @@ -563,12 +564,12 @@ class HintManager(QObject): if target in [Target.userscript, Target.spawn, Target.run, Target.fill]: if not args: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "'args' is required with target userscript/spawn/run/" "fill.") else: if args: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "'args' is only allowed with target userscript/spawn.") def _filter_matches(self, filterstr, elemstr): @@ -705,7 +706,7 @@ class HintManager(QObject): window=self._win_id) tab = tabbed_browser.widget.currentWidget() if tab is None: - raise cmdexc.CommandError("No WebView available yet!") + raise cmdutils.CommandError("No WebView available yet!") mode_manager = objreg.get('mode-manager', scope='window', window=self._win_id) @@ -722,8 +723,8 @@ class HintManager(QObject): pass else: name = target.name.replace('_', '-') - raise cmdexc.CommandError("Rapid hinting makes no sense with " - "target {}!".format(name)) + raise cmdutils.CommandError("Rapid hinting makes no sense " + "with target {}!".format(name)) self._check_args(target, *args) self._context = HintContext() @@ -736,7 +737,7 @@ class HintManager(QObject): try: self._context.baseurl = tabbed_browser.current_url() except qtutils.QtValueError: - raise cmdexc.CommandError("No URL set for this page yet!") + raise cmdutils.CommandError("No URL set for this page yet!") self._context.args = list(args) self._context.group = group @@ -744,7 +745,7 @@ class HintManager(QObject): selector = webelem.css_selector(self._context.group, self._context.baseurl) except webelem.Error as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) self._context.tab.elements.find_css(selector, self._start_cb, only_visible=True) @@ -758,7 +759,7 @@ class HintManager(QObject): try: opt.typ.to_py(mode) except configexc.ValidationError as e: - raise cmdexc.CommandError("Invalid mode: {}".format(e)) + raise cmdutils.CommandError("Invalid mode: {}".format(e)) return mode def current_mode(self): @@ -960,13 +961,13 @@ class HintManager(QObject): """ if keystring is None: if self._context.to_follow is None: - raise cmdexc.CommandError("No hint to follow") + raise cmdutils.CommandError("No hint to follow") elif select: - raise cmdexc.CommandError("Can't use --select without hint.") + raise cmdutils.CommandError("Can't use --select without hint.") else: keystring = self._context.to_follow elif keystring not in self._context.labels: - raise cmdexc.CommandError("No hint {}!".format(keystring)) + raise cmdutils.CommandError("No hint {}!".format(keystring)) if select: self.handle_partial_key(keystring) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index f98ec51a5..757a72e41 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal from PyQt5.QtWidgets import QProgressDialog, QApplication from qutebrowser.config import config -from qutebrowser.commands import cmdutils, cmdexc +from qutebrowser.api import cmdutils from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils from qutebrowser.misc import objects, sql @@ -365,7 +365,8 @@ class WebHistory(sql.SqlTable): f.write('\n'.join(lines)) message.info("Dumped history to {}".format(dest)) except OSError as e: - raise cmdexc.CommandError('Could not write history: {}'.format(e)) + raise cmdutils.CommandError('Could not write history: {}' + .format(e)) def init(parent=None): diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index e32dbcc46..4d25dde45 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -35,7 +35,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl, QObject from qutebrowser.utils import (message, usertypes, qtutils, urlutils, standarddir, objreg, log) -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils from qutebrowser.misc import lineparser @@ -166,7 +166,7 @@ class QuickmarkManager(UrlMarkManager): url: The url to add as quickmark. name: The name for the new quickmark. """ - # We don't raise cmdexc.CommandError here as this can be called async + # We don't raise cmdutils.CommandError here as this can be called async # via prompt_save. if not name: message.error("Can't set mark with empty name!") diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index dbc5da581..f342f2436 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -28,11 +28,6 @@ class Error(Exception): """Base class for all cmdexc errors.""" -class CommandError(Error): - - """Raised when a command encounters an error while running.""" - - class NoSuchCommandError(Error): """Raised when a command wasn't found.""" diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 86b0cc3a2..000689a75 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -25,6 +25,7 @@ import re import attr from PyQt5.QtCore import pyqtSlot, QUrl, QObject +from qutebrowser.api import cmdutils from qutebrowser.config import config from qutebrowser.commands import cmdexc from qutebrowser.utils import message, objreg, qtutils, usertypes, utils @@ -53,7 +54,7 @@ def _current_url(tabbed_browser): if e.reason: msg += " ({})".format(e.reason) msg += "!" - raise cmdexc.CommandError(msg) + raise cmdutils.CommandError(msg) def replace_variables(win_id, arglist): @@ -93,7 +94,7 @@ def replace_variables(win_id, arglist): # "{url}" from clipboard is not expanded) args.append(repl_pattern.sub(repl_cb, arg)) except utils.ClipboardError as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) return args diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 740be75d9..0af4ecbe1 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config from qutebrowser.completion import completiondelegate from qutebrowser.utils import utils, usertypes, debug, log, objreg -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils class CompletionView(QTreeView): @@ -251,8 +251,8 @@ class CompletionView(QTreeView): status.command_history_prev() return else: - raise cmdexc.CommandError("Can't combine --history with " - "{}!".format(which)) + raise cmdutils.CommandError("Can't combine --history with " + "{}!".format(which)) if not self._active: return @@ -394,7 +394,7 @@ class CompletionView(QTreeView): """Delete the current completion item.""" index = self.currentIndex() if not index.isValid(): - raise cmdexc.CommandError("No item selected!") + raise cmdutils.CommandError("No item selected!") self.model().delete_cur_item(index) @cmdutils.register(instance='completion', @@ -411,6 +411,6 @@ class CompletionView(QTreeView): if not text: index = self.currentIndex() if not index.isValid(): - raise cmdexc.CommandError("No item selected!") + raise cmdutils.CommandError("No item selected!") text = self.model().data(index) utils.set_clipboard(text, selection=sel) diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py index 1c77e1d31..36a465fb7 100644 --- a/qutebrowser/completion/models/completionmodel.py +++ b/qutebrowser/completion/models/completionmodel.py @@ -22,7 +22,7 @@ from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel from qutebrowser.utils import log, qtutils -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils class CompletionModel(QAbstractItemModel): @@ -224,7 +224,7 @@ class CompletionModel(QAbstractItemModel): cat = self._cat_from_idx(parent) assert cat, "CompletionView sent invalid index for deletion" if not cat.delete_func: - raise cmdexc.CommandError("Cannot delete this item.") + raise cmdutils.CommandError("Cannot delete this item.") data = [cat.data(cat.index(index.row(), i)) for i in range(cat.columnCount())] diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 8853f9ca3..8587acc3f 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -24,7 +24,7 @@ import contextlib from PyQt5.QtCore import QUrl -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils from qutebrowser.completion.models import configmodel from qutebrowser.utils import objreg, message, standarddir, urlmatch from qutebrowser.config import configtypes, configexc, configfiles, configdata @@ -46,7 +46,7 @@ class ConfigCommands: try: yield except configexc.Error as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) def _parse_pattern(self, pattern): """Parse a pattern string argument to a pattern.""" @@ -56,15 +56,15 @@ class ConfigCommands: try: return urlmatch.UrlPattern(pattern) except urlmatch.ParseError as e: - raise cmdexc.CommandError("Error while parsing {}: {}" - .format(pattern, str(e))) + raise cmdutils.CommandError("Error while parsing {}: {}" + .format(pattern, str(e))) def _parse_key(self, key): """Parse a key argument.""" try: return keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) def _print_value(self, option, pattern): """Print the value of the given option.""" @@ -105,8 +105,8 @@ class ConfigCommands: return if option.endswith('!'): - raise cmdexc.CommandError("Toggling values was moved to the " - ":config-cycle command") + raise cmdutils.CommandError("Toggling values was moved to the " + ":config-cycle command") pattern = self._parse_pattern(pattern) @@ -213,8 +213,8 @@ class ConfigCommands: values = ['true', 'false'] if len(values) < 2: - raise cmdexc.CommandError("Need at least two values for " - "non-boolean settings.") + raise cmdutils.CommandError("Need at least two values for " + "non-boolean settings.") # Use the next valid value from values, or the first if the current # value does not appear in the list @@ -263,8 +263,8 @@ class ConfigCommands: opt = self._config.get_opt(option) valid_list_types = (configtypes.List, configtypes.ListOrValue) if not isinstance(opt.typ, valid_list_types): - raise cmdexc.CommandError(":config-list-add can only be used for " - "lists") + raise cmdutils.CommandError(":config-list-add can only be used " + "for lists") with self._handle_config_error(): option_value = self._config.get_mutable_obj(option) @@ -286,16 +286,16 @@ class ConfigCommands: """ opt = self._config.get_opt(option) if not isinstance(opt.typ, configtypes.Dict): - raise cmdexc.CommandError(":config-dict-add can only be used for " - "dicts") + raise cmdutils.CommandError(":config-dict-add can only be used " + "for dicts") with self._handle_config_error(): option_value = self._config.get_mutable_obj(option) if key in option_value and not replace: - raise cmdexc.CommandError("{} already exists in {} - use " - "--replace to overwrite!" - .format(key, option)) + raise cmdutils.CommandError("{} already exists in {} - use " + "--replace to overwrite!" + .format(key, option)) option_value[key] = value self._config.update_mutables(save_yaml=not temp) @@ -313,15 +313,15 @@ class ConfigCommands: opt = self._config.get_opt(option) valid_list_types = (configtypes.List, configtypes.ListOrValue) if not isinstance(opt.typ, valid_list_types): - raise cmdexc.CommandError(":config-list-remove can only be used " - "for lists") + raise cmdutils.CommandError(":config-list-remove can only be used " + "for lists") with self._handle_config_error(): option_value = self._config.get_mutable_obj(option) if value not in option_value: - raise cmdexc.CommandError("{} is not in {}!".format(value, - option)) + raise cmdutils.CommandError("{} is not in {}!".format( + value, option)) option_value.remove(value) @@ -339,15 +339,15 @@ class ConfigCommands: """ opt = self._config.get_opt(option) if not isinstance(opt.typ, configtypes.Dict): - raise cmdexc.CommandError(":config-dict-remove can only be used " - "for dicts") + raise cmdutils.CommandError(":config-dict-remove can only be used " + "for dicts") with self._handle_config_error(): option_value = self._config.get_mutable_obj(option) if key not in option_value: - raise cmdexc.CommandError("{} is not in {}!".format(key, - option)) + raise cmdutils.CommandError("{} is not in {}!".format( + key, option)) del option_value[key] @@ -383,7 +383,7 @@ class ConfigCommands: try: configfiles.read_config_py(filename) except configexc.ConfigFileErrors as e: - raise cmdexc.CommandError(e) + raise cmdutils.CommandError(e) @cmdutils.register(instance='config-commands') def config_edit(self, no_source=False): @@ -395,7 +395,7 @@ class ConfigCommands: def on_file_updated(): """Source the new config when editing finished. - This can't use cmdexc.CommandError as it's run async. + This can't use cmdutils.CommandError as it's run async. """ try: configfiles.read_config_py(filename) @@ -426,8 +426,8 @@ class ConfigCommands: filename = os.path.expanduser(filename) if os.path.exists(filename) and not force: - raise cmdexc.CommandError("{} already exists - use --force to " - "overwrite!".format(filename)) + raise cmdutils.CommandError("{} already exists - use --force to " + "overwrite!".format(filename)) if defaults: options = [(None, opt, opt.default) @@ -447,4 +447,4 @@ class ConfigCommands: try: writer.write(filename) except OSError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 97b0b103b..bd17f5664 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -19,7 +19,8 @@ """Keyboard macro system.""" -from qutebrowser.commands import cmdexc, cmdutils, runners +from qutebrowser.commands import runners +from qutebrowser.api import cmdutils from qutebrowser.keyinput import modeman from qutebrowser.utils import message, objreg, usertypes @@ -89,12 +90,12 @@ class MacroRecorder: """Run a recorded macro.""" if register == '@': if self._last_register is None: - raise cmdexc.CommandError("No previous macro") + raise cmdutils.CommandError("No previous macro") register = self._last_register self._last_register = register if register not in self._macros: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "No macro recorded in '{}'!".format(register)) commandrunner = runners.CommandRunner(win_id) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index c06f18a1c..c06700b6c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -27,7 +27,7 @@ from PyQt5.QtWidgets import QApplication from qutebrowser.keyinput import modeparsers from qutebrowser.config import config -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, log, objreg, utils INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough] @@ -282,11 +282,11 @@ class ModeManager(QObject): try: m = usertypes.KeyMode[mode] except KeyError: - raise cmdexc.CommandError("Mode {} does not exist!".format(mode)) + raise cmdutils.CommandError("Mode {} does not exist!".format(mode)) if m in [usertypes.KeyMode.hint, usertypes.KeyMode.command, usertypes.KeyMode.yesno, usertypes.KeyMode.prompt]: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "Mode {} can't be entered manually!".format(mode)) self.enter(m, 'command') diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 3fdad13d1..ed0a78469 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -28,7 +28,8 @@ from PyQt5.QtCore import (pyqtSlot, QRect, QPoint, QTimer, Qt, QCoreApplication, QEventLoop) from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy -from qutebrowser.commands import runners, cmdutils +from qutebrowser.commands import runners +from qutebrowser.api import cmdutils from qutebrowser.config import config, configfiles from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, jinja) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index f6a8b1224..f666aa837 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -34,7 +34,7 @@ from qutebrowser.browser import downloads from qutebrowser.config import config from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.keyinput import modeman -from qutebrowser.commands import cmdutils, cmdexc +from qutebrowser.api import cmdutils from qutebrowser.qt import sip @@ -384,7 +384,7 @@ class PromptContainer(QWidget): try: done = self._prompt.accept(value) except Error as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) if done: message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE) question.done() diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index c04dab03e..de42cda4e 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize from PyQt5.QtWidgets import QSizePolicy from qutebrowser.keyinput import modeman, modeparsers -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils from qutebrowser.misc import cmdhistory, editor from qutebrowser.misc import miscwidgets as misc from qutebrowser.utils import usertypes, log, objreg, message, utils @@ -137,11 +137,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): text += ' ' if append: if not self.text(): - raise cmdexc.CommandError("No current text!") + raise cmdutils.CommandError("No current text!") text = self.text() + text if not text or text[0] not in modeparsers.STARTCHARS: - raise cmdexc.CommandError( + raise cmdutils.CommandError( "Invalid command text '{}'.".format(text)) if run_on_count and count is not None: self.got_cmd[str, int].emit(text, count) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 196613d62..7890380e8 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -37,7 +37,7 @@ import attr from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils from qutebrowser.misc import earlyinit, crashdialog, ipc from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils diff --git a/qutebrowser/misc/readline.py b/qutebrowser/misc/readline.py index 3846b77e0..14c25cd6d 100644 --- a/qutebrowser/misc/readline.py +++ b/qutebrowser/misc/readline.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import QApplication, QLineEdit -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes as typ from qutebrowser.utils import utils diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 0d79c97db..9985c5191 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -25,7 +25,7 @@ import collections from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import cmdutils +from qutebrowser.api import cmdutils from qutebrowser.utils import utils, log, message, usertypes diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 459011e0f..4f80ad2b0 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -31,7 +31,7 @@ import yaml from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, utils) -from qutebrowser.commands import cmdexc, cmdutils +from qutebrowser.api import cmdutils from qutebrowser.config import config, configfiles from qutebrowser.completion.models import miscmodels from qutebrowser.mainwindow import mainwindow @@ -480,16 +480,17 @@ class SessionManager(QObject): delete: Delete the saved session once it has loaded. """ if name.startswith('_') and not force: - raise cmdexc.CommandError("{} is an internal session, use --force " - "to load anyways.".format(name)) + raise cmdutils.CommandError("{} is an internal session, use " + "--force to load anyways." + .format(name)) old_windows = list(objreg.window_registry.values()) try: self.load(name, temp=temp) except SessionNotFoundError: - raise cmdexc.CommandError("Session {} not found!".format(name)) + raise cmdutils.CommandError("Session {} not found!".format(name)) except SessionError as e: - raise cmdexc.CommandError("Error while loading session: {}" - .format(e)) + raise cmdutils.CommandError("Error while loading session: {}" + .format(e)) else: if clear: for win in old_windows: @@ -499,9 +500,8 @@ class SessionManager(QObject): self.delete(name) except SessionError as e: log.sessions.exception("Error while deleting session!") - raise cmdexc.CommandError( - "Error while deleting session: {}" - .format(e)) + raise cmdutils.CommandError("Error while deleting " + "session: {}".format(e)) else: log.sessions.debug( "Loaded & deleted session {}.".format(name)) @@ -531,11 +531,12 @@ class SessionManager(QObject): if (not isinstance(name, Sentinel) and name.startswith('_') and not force): - raise cmdexc.CommandError("{} is an internal session, use --force " - "to save anyways.".format(name)) + raise cmdutils.CommandError("{} is an internal session, use " + "--force to save anyways." + .format(name)) if current: if self._current is None: - raise cmdexc.CommandError("No session loaded currently!") + raise cmdutils.CommandError("No session loaded currently!") name = self._current assert not name.startswith('_') try: @@ -545,8 +546,8 @@ class SessionManager(QObject): else: name = self.save(name, with_private=with_private) except SessionError as e: - raise cmdexc.CommandError("Error while saving session: {}" - .format(e)) + raise cmdutils.CommandError("Error while saving session: {}" + .format(e)) else: if quiet: log.sessions.debug("Saved session {}.".format(name)) @@ -564,15 +565,16 @@ class SessionManager(QObject): underline). """ if name.startswith('_') and not force: - raise cmdexc.CommandError("{} is an internal session, use --force " - "to delete anyways.".format(name)) + raise cmdutils.CommandError("{} is an internal session, use " + "--force to delete anyways." + .format(name)) try: self.delete(name) except SessionNotFoundError: - raise cmdexc.CommandError("Session {} not found!".format(name)) + raise cmdutils.CommandError("Session {} not found!".format(name)) except SessionError as e: log.sessions.exception("Error while deleting session!") - raise cmdexc.CommandError("Error while deleting session: {}" - .format(e)) + raise cmdutils.CommandError("Error while deleting session: {}" + .format(e)) else: log.sessions.debug("Deleted session {}.".format(name)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 003fb494c..e78b6d9b7 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -35,7 +35,8 @@ from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import from qutebrowser.browser import qutescheme from qutebrowser.utils import log, objreg, usertypes, message, debug, utils -from qutebrowser.commands import cmdutils, runners, cmdexc +from qutebrowser.commands import runners +from qutebrowser.api import cmdutils from qutebrowser.config import config, configdata from qutebrowser.misc import consolewidget from qutebrowser.utils.version import pastebin_version @@ -52,7 +53,7 @@ def later(ms: int, command: str, win_id: int) -> None: command: The command to run, with optional args. """ if ms < 0: - raise cmdexc.CommandError("I can't run something in the past!") + raise cmdutils.CommandError("I can't run something in the past!") commandrunner = runners.CommandRunner(win_id) app = objreg.get('app') timer = usertypes.Timer(name='later', parent=app) @@ -61,8 +62,8 @@ def later(ms: int, command: str, win_id: int) -> None: try: timer.setInterval(ms) except OverflowError: - raise cmdexc.CommandError("Numeric argument is too large for " - "internal int representation.") + raise cmdutils.CommandError("Numeric argument is too large for " + "internal int representation.") timer.timeout.connect( functools.partial(commandrunner.run_safely, command)) timer.timeout.connect(timer.deleteLater) @@ -87,7 +88,7 @@ def repeat(times: int, command: str, win_id: int, count: int = None) -> None: times *= count if times < 0: - raise cmdexc.CommandError("A negative count doesn't make sense.") + raise cmdutils.CommandError("A negative count doesn't make sense.") commandrunner = runners.CommandRunner(win_id) for _ in range(times): commandrunner.run_safely(command) @@ -227,12 +228,12 @@ def debug_trace(expr=""): expr: What to trace, passed to hunter. """ if hunter is None: - raise cmdexc.CommandError("You need to install 'hunter' to use this " - "command!") + raise cmdutils.CommandError("You need to install 'hunter' to use this " + "command!") try: eval('hunter.trace({})'.format(expr)) except Exception as e: - raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e)) + raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e)) @cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) @@ -251,7 +252,7 @@ def debug_pyeval(s, file=False, quiet=False): with open(path, 'r', encoding='utf-8') as f: s = f.read() except OSError as e: - raise cmdexc.CommandError(str(e)) + raise cmdutils.CommandError(str(e)) try: exec(s) out = "No error" @@ -297,7 +298,7 @@ def repeat_command(win_id, count=None): """ mode_manager = objreg.get('mode-manager', scope='window', window=win_id) if mode_manager.mode not in runners.last_command: - raise cmdexc.CommandError("You didn't do anything yet.") + raise cmdutils.CommandError("You didn't do anything yet.") cmd = runners.last_command[mode_manager.mode] commandrunner = runners.CommandRunner(win_id) commandrunner.run(cmd[0], count if count is not None else cmd[1]) @@ -311,7 +312,7 @@ def log_capacity(capacity: int) -> None: capacity: Number of lines for the log. """ if capacity < 0: - raise cmdexc.CommandError("Can't set a negative log capacity!") + raise cmdutils.CommandError("Can't set a negative log capacity!") else: assert log.ram_handler is not None log.ram_handler.change_log_capacity(capacity) @@ -341,17 +342,17 @@ def debug_log_filter(filters: str) -> None: clear any existing filters. """ if log.console_filter is None: - raise cmdexc.CommandError("No log.console_filter. Not attached " - "to a console?") + raise cmdutils.CommandError("No log.console_filter. Not attached " + "to a console?") if filters.strip().lower() == 'none': log.console_filter.names = None return if not set(filters.split(',')).issubset(log.LOGGER_NAMES): - raise cmdexc.CommandError("filters: Invalid value {} - expected one " - "of: {}".format(filters, - ', '.join(log.LOGGER_NAMES))) + raise cmdutils.CommandError("filters: Invalid value {} - expected one " + "of: {}".format( + filters, ', '.join(log.LOGGER_NAMES))) log.console_filter.names = filters.split(',') diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 00cf4d01a..1acd9cd4e 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -29,9 +29,9 @@ import urllib.parse from PyQt5.QtCore import QUrl, QUrlQuery from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy +from qutebrowser.api import cmdutils from qutebrowser.config import config from qutebrowser.utils import log, qtutils, message, utils -from qutebrowser.commands import cmdexc from qutebrowser.browser.network import pac @@ -361,7 +361,7 @@ def invalid_url_error(url, action): def raise_cmdexc_if_invalid(url): """Check if the given QUrl is invalid, and if so, raise a CommandError.""" if not url.isValid(): - raise cmdexc.CommandError(get_errstring(url)) + raise cmdutils.CommandError(get_errstring(url)) def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False): diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 5c678ac96..21ebd6557 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -53,8 +53,8 @@ MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file') PERFECT_FILES = [ (None, 'commands/cmdexc.py'), - ('tests/unit/commands/test_cmdutils.py', - 'commands/cmdutils.py'), + ('tests/unit/api/test_cmdutils.py', + 'api/cmdutils.py'), ('tests/unit/commands/test_argparser.py', 'commands/argparser.py'), diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/api/test_cmdutils.py similarity index 98% rename from tests/unit/commands/test_cmdutils.py rename to tests/unit/api/test_cmdutils.py index 4d7877e69..f2318ab46 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -19,7 +19,7 @@ # pylint: disable=unused-variable -"""Tests for qutebrowser.commands.cmdutils.""" +"""Tests for qutebrowser.api.cmdutils.""" import sys import logging @@ -30,7 +30,8 @@ import enum import pytest from qutebrowser.misc import objects -from qutebrowser.commands import cmdexc, argparser, command, cmdutils +from qutebrowser.commands import cmdexc, argparser, command +from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes @@ -59,7 +60,7 @@ class TestCheckOverflow: def test_bad(self): int32_max = 2 ** 31 - 1 - with pytest.raises(cmdexc.CommandError, match="Numeric argument is " + with pytest.raises(cmdutils.CommandError, match="Numeric argument is " "too large for internal int representation."): cmdutils.check_overflow(int32_max + 1, 'int') @@ -71,7 +72,7 @@ class TestCheckExclusive: cmdutils.check_exclusive(flags, []) def test_bad(self): - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="Only one of -x/-y/-z can be given!"): cmdutils.check_exclusive([True, True], 'xyz') diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 5b84eac4c..715b597b0 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.browser import history from qutebrowser.utils import objreg, urlutils, usertypes -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils from qutebrowser.misc import sql @@ -324,7 +324,7 @@ class TestDump: def test_nonexistent(self, web_history, tmpdir): histfile = tmpdir / 'nonexistent' / 'history' - with pytest.raises(cmdexc.CommandError): + with pytest.raises(cmdutils.CommandError): web_history.debug_dump_history(str(histfile)) diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 59291cdd6..16ebb95dc 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -26,7 +26,8 @@ from PyQt5.QtCore import QObject from PyQt5.QtGui import QStandardItemModel from qutebrowser.completion import completer -from qutebrowser.commands import command, cmdutils +from qutebrowser.commands import command +from qutebrowser.api import cmdutils class FakeCompletionModel(QStandardItemModel): diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index e0e044ffb..24f5bdf0d 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -28,7 +28,7 @@ from PyQt5.QtCore import QModelIndex from qutebrowser.completion.models import completionmodel, listcategory from qutebrowser.utils import qtutils -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils @hypothesis.given(strategies.lists( @@ -102,7 +102,7 @@ def test_delete_cur_item_no_func(): model.rowsRemoved.connect(callback) model.add_category(cat) parent = model.index(0, 0) - with pytest.raises(cmdexc.CommandError): + with pytest.raises(cmdutils.CommandError): model.delete_cur_item(model.index(0, 0, parent)) callback.assert_not_called() diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 01bd3ec03..3d66e1145 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -25,7 +25,7 @@ import pytest from qutebrowser.completion import completionwidget from qutebrowser.completion.models import completionmodel, listcategory -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils @pytest.fixture @@ -241,7 +241,7 @@ def test_completion_item_del_no_selection(completionview): cat = listcategory.ListCategory('', [('foo',)], delete_func=func) model.add_category(cat) completionview.set_model(model) - with pytest.raises(cmdexc.CommandError, match='No item selected!'): + with pytest.raises(cmdutils.CommandError, match='No item selected!'): completionview.completion_item_del() func.assert_not_called() diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 47bcaaa74..001d55899 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -26,7 +26,7 @@ import pytest from PyQt5.QtCore import QUrl from qutebrowser.config import configcommands, configutils -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, urlmatch from qutebrowser.keyinput import keyutils from qutebrowser.misc import objects @@ -108,7 +108,7 @@ class TestSet: monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit) option = 'content.javascript.enabled' - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match=('Error while parsing http://: Pattern ' 'without host')): commands.set(0, option, 'false', pattern='http://') @@ -118,7 +118,7 @@ class TestSet: Should show an error as patterns are unsupported. """ - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match='does not support URL patterns'): commands.set(0, 'colors.statusbar.normal.bg', '#abcdef', pattern='*://*') @@ -165,7 +165,7 @@ class TestSet: Should show an error. """ - with pytest.raises(cmdexc.CommandError, match="No option 'foo'"): + with pytest.raises(cmdutils.CommandError, match="No option 'foo'"): commands.set(0, 'foo', 'bar') def test_set_invalid_value(self, commands): @@ -173,13 +173,13 @@ class TestSet: Should show an error. """ - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="Invalid value 'blah' - must be a boolean!"): commands.set(0, 'auto_save.session', 'blah') def test_set_wrong_backend(self, commands, monkeypatch): monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="The hints.find_implementation setting is " "not available with the QtWebEngine backend!"): commands.set(0, 'hints.find_implementation', 'javascript') @@ -190,7 +190,7 @@ class TestSet: Should show an error. See https://github.com/qutebrowser/qutebrowser/issues/1109 """ - with pytest.raises(cmdexc.CommandError, match="No option '?'"): + with pytest.raises(cmdutils.CommandError, match="No option '?'"): commands.set(win_id=0, option='?') def test_toggle(self, commands): @@ -198,7 +198,7 @@ class TestSet: Should show an nicer error. """ - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="Toggling values was moved to the " ":config-cycle command"): commands.set(win_id=0, option='javascript.enabled!') @@ -208,7 +208,7 @@ class TestSet: Should show an error. """ - with pytest.raises(cmdexc.CommandError, match="No option 'foo'"): + with pytest.raises(cmdutils.CommandError, match="No option 'foo'"): commands.set(win_id=0, option='foo?') @@ -267,7 +267,7 @@ class TestCycle: Should show an error. """ assert config_stub.val.url.auto_search == 'naive' - with pytest.raises(cmdexc.CommandError, match="Need at least " + with pytest.raises(cmdutils.CommandError, match="Need at least " "two values for non-boolean settings."): commands.config_cycle(*args) assert config_stub.val.url.auto_search == 'naive' @@ -301,14 +301,14 @@ class TestAdd: def test_list_add_non_list(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match=":config-list-add can only be used for lists"): commands.config_list_add('history_gap_interval', 'value') @pytest.mark.parametrize('value', ['', None, 42]) def test_list_add_invalid_values(self, commands, value): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match="Invalid value '{}'".format(value)): commands.config_list_add('content.host_blocking.whitelist', value) @@ -337,20 +337,20 @@ class TestAdd: assert str(config_stub.get(name)[key]) == value else: with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match="w already exists in aliases - use --replace to " "overwrite!"): commands.config_dict_add(name, key, value, replace=False) def test_dict_add_non_dict(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match=":config-dict-add can only be used for dicts"): commands.config_dict_add('history_gap_interval', 'key', 'value') @pytest.mark.parametrize('value', ['', None, 42]) def test_dict_add_invalid_values(self, commands, value): - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="Invalid value '{}'".format(value)): commands.config_dict_add('aliases', 'missingkey', value) @@ -373,14 +373,14 @@ class TestRemove: def test_list_remove_non_list(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match=":config-list-remove can only be used for lists"): commands.config_list_remove('content.javascript.enabled', 'never') def test_list_remove_no_value(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match="never is not in colors.completion.fg!"): commands.config_list_remove('colors.completion.fg', 'never') @@ -398,14 +398,14 @@ class TestRemove: def test_dict_remove_non_dict(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match=":config-dict-remove can only be used for dicts"): commands.config_dict_remove('content.javascript.enabled', 'never') def test_dict_remove_no_value(self, commands): with pytest.raises( - cmdexc.CommandError, + cmdutils.CommandError, match="never is not in aliases!"): commands.config_dict_remove('aliases', 'never') @@ -425,7 +425,7 @@ class TestUnsetAndClear: assert yaml_value(name) == ('never' if temp else configutils.UNSET) def test_unset_unknown_option(self, commands): - with pytest.raises(cmdexc.CommandError, match="No option 'tabs'"): + with pytest.raises(cmdutils.CommandError, match="No option 'tabs'"): commands.config_unset('tabs') @pytest.mark.parametrize('save', [True, False]) @@ -472,7 +472,7 @@ class TestSource: pyfile = config_tmpdir / 'config.py' pyfile.write_text('c.foo = 42', encoding='utf-8') - with pytest.raises(cmdexc.CommandError) as excinfo: + with pytest.raises(cmdutils.CommandError) as excinfo: commands.config_source() expected = ("Errors occurred while reading config.py:\n" @@ -483,7 +483,7 @@ class TestSource: pyfile = config_tmpdir / 'config.py' pyfile.write_text('1/0', encoding='utf-8') - with pytest.raises(cmdexc.CommandError) as excinfo: + with pytest.raises(cmdutils.CommandError) as excinfo: commands.config_source() expected = ("Errors occurred while reading config.py:\n" @@ -582,7 +582,7 @@ class TestWritePy: confpy = tmpdir / 'config.py' confpy.ensure() - with pytest.raises(cmdexc.CommandError) as excinfo: + with pytest.raises(cmdutils.CommandError) as excinfo: commands.config_write_py(str(confpy)) expected = " already exists - use --force to overwrite!" @@ -599,7 +599,7 @@ class TestWritePy: def test_oserror(self, commands, tmpdir): """Test writing to a directory which does not exist.""" - with pytest.raises(cmdexc.CommandError): + with pytest.raises(cmdutils.CommandError): commands.config_write_py(str(tmpdir / 'foo' / 'config.py')) @@ -709,7 +709,7 @@ class TestBind: elif command == 'unbind': func = commands.unbind - with pytest.raises(cmdexc.CommandError, match=expected): + with pytest.raises(cmdutils.CommandError, match=expected): func(*args, **kwargs) @pytest.mark.parametrize('key', ['a', 'b', '']) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index cfa115412..73f97095f 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -28,7 +28,7 @@ import pytest from PyQt5.QtCore import QUrl from qutebrowser.misc import utilcmds -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils from qutebrowser.utils import utils, objreg @@ -83,14 +83,14 @@ def test_debug_trace_exception(mocker): hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') hunter_mock.trace.side_effect = _mock_exception - with pytest.raises(cmdexc.CommandError, match='Exception: message'): + with pytest.raises(cmdutils.CommandError, match='Exception: message'): utilcmds.debug_trace() def test_debug_trace_no_hunter(monkeypatch): """Test that an error is shown if debug_trace is called without hunter.""" monkeypatch.setattr(utilcmds, 'hunter', None) - with pytest.raises(cmdexc.CommandError, match="You need to install " + with pytest.raises(cmdutils.CommandError, match="You need to install " "'hunter' to use this command!"): utilcmds.debug_trace() @@ -103,7 +103,7 @@ def test_repeat_command_initial(mocker, mode_manager): """ objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg') objreg_mock.get.return_value = mode_manager - with pytest.raises(cmdexc.CommandError, + with pytest.raises(cmdutils.CommandError, match="You didn't do anything yet."): utilcmds.repeat_command(win_id=0) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 26b063456..1c1efffab 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkProxy import pytest -from qutebrowser.commands import cmdexc +from qutebrowser.api import cmdutils from qutebrowser.browser.network import pac from qutebrowser.utils import utils, urlutils, qtutils, usertypes from helpers import utils as testutils @@ -495,7 +495,7 @@ def test_raise_cmdexc_if_invalid(url, valid, has_err_string): expected_text = "Invalid URL - " + qurl.errorString() else: expected_text = "Invalid URL" - with pytest.raises(cmdexc.CommandError, match=expected_text): + with pytest.raises(cmdutils.CommandError, match=expected_text): urlutils.raise_cmdexc_if_invalid(qurl) From 6ccba16b728898e2a8aa2026303cc06627d58636 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:02:03 +0100 Subject: [PATCH 097/258] Fix coverage --- qutebrowser/misc/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index 0bb26954c..ccb5d09b3 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -25,7 +25,7 @@ import typing MYPY = False -if MYPY: +if MYPY: # pragma: no cover # pylint: disable=unused-import,useless-suppression from qutebrowser.utils import usertypes from qutebrowser.commands import command From c7179e35cc34a883e135aa5dc4a1b9907a1a87f4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:02:36 +0100 Subject: [PATCH 098/258] Remove old code --- qutebrowser/browser/browsertab.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 1ced63fab..48d69b198 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -349,13 +349,6 @@ class AbstractZoom(QObject): config.instance.changed.connect(self._on_config_changed) self._zoom_factor = float(config.val.zoom.default) / 100 - # # FIXME:qtwebengine is this needed? - # # For some reason, this signal doesn't get disconnected automatically - # # when the WebView is destroyed on older PyQt versions. - # # See https://github.com/qutebrowser/qutebrowser/issues/390 - # self.destroyed.connect(functools.partial( - # cfg.changed.disconnect, self.init_neighborlist)) - @pyqtSlot(str) def _on_config_changed(self, option: str) -> None: if option in ['zoom.levels', 'zoom.default']: From 595e1527291e5fb094e7266facbfde5cc7c2dfc6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:03:45 +0100 Subject: [PATCH 099/258] Remove AbstractAudio.toggle_muted --- qutebrowser/browser/browsertab.py | 3 --- qutebrowser/browser/commands.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 48d69b198..690c978ff 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -720,9 +720,6 @@ class AbstractAudio(QObject): """Whether this tab is muted.""" raise NotImplementedError - def toggle_muted(self, *, override: bool = False) -> None: - self.set_muted(not self.is_muted(), override=override) - def is_recently_audible(self) -> bool: """Whether this tab has had audio playing recently.""" raise NotImplementedError diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index df6906515..9ad5e0011 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2267,6 +2267,6 @@ class CommandDispatcher: if tab is None: return try: - tab.audio.toggle_muted(override=True) + tab.audio.set_muted(tab.audio.is_muted(), override=True) except browsertab.WebTabError as e: raise cmdutils.CommandError(e) From c1b9318e7759bcf5a77c262f3115e7593b3e7e95 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:04:14 +0100 Subject: [PATCH 100/258] check_coverage: Add test_objects.py --- scripts/dev/check_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 21ebd6557..8d0647b0d 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -113,7 +113,7 @@ PERFECT_FILES = [ 'misc/keyhintwidget.py'), ('tests/unit/misc/test_pastebin.py', 'misc/pastebin.py'), - (None, + ('tests/unit/misc/test_objects.py', 'misc/objects.py'), (None, From 4b7c60da9fc68de6d56123a250ef3f966c4fd348 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:07:13 +0100 Subject: [PATCH 101/258] Improve docstrings --- qutebrowser/browser/browsertab.py | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 690c978ff..1f2be1476 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -206,15 +206,31 @@ class AbstractPrinting: self._tab = tab def check_pdf_support(self) -> bool: + """Check whether writing to PDFs is supported. + + If it's not supported (by the current Qt version), a WebTabError is + raised. + """ raise NotImplementedError def check_printer_support(self) -> bool: + """Check whether writing to a printer is supported. + + If it's not supported (by the current Qt version), a WebTabError is + raised. + """ raise NotImplementedError def check_preview_support(self) -> bool: + """Check whether showing a print preview is supported. + + If it's not supported (by the current Qt version), a WebTabError is + raised. + """ raise NotImplementedError def to_pdf(self, filename: str) -> bool: + """Print the tab to a PDF with the given filename.""" raise NotImplementedError def to_printer(self, printer: QPrinter, @@ -668,6 +684,7 @@ class AbstractElements: Args: callback: The callback to be called when the search finished. + Called with a WebEngineElement or None. elem_id: The ID to search for. """ raise NotImplementedError @@ -717,7 +734,6 @@ class AbstractAudio(QObject): raise NotImplementedError def is_muted(self) -> bool: - """Whether this tab is muted.""" raise NotImplementedError def is_recently_audible(self) -> bool: @@ -788,21 +804,7 @@ class AbstractTabPrivate: class AbstractTab(QWidget): - """A wrapper over the given widget to hide its API and expose another one. - - We use this to unify QWebView and QWebEngineView. - - Attributes: - history: The AbstractHistory for the current tab. - registry: The ObjectRegistry associated with this tab. - is_private: Whether private browsing is turned on for this tab. - - _load_status: loading status of this page - Accessible via load_status() method. - _has_ssl_errors: Whether SSL errors happened. - Needs to be set by subclasses. - - for properties, see WebView/WebEngineView docs. + """An adapter for QWebView/QWebEngineView representing a single tab. Signals: See related Qt signals. @@ -902,7 +904,7 @@ class AbstractTab(QWidget): """Send the given event to the underlying widget. The event will be sent via QApplication.postEvent. - Note that a posted event may not be re-used in any way! + Note that a posted event must not be re-used in any way! """ # This only gives us some mild protection against re-using events, but # it's certainly better than a segfault. From f81c5b7eab3b757dfdc51caf519f829a8570289e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:09:41 +0100 Subject: [PATCH 102/258] Move common key_press implementation --- qutebrowser/browser/browsertab.py | 8 ++++++-- qutebrowser/browser/webengine/webenginetab.py | 9 +-------- qutebrowser/browser/webkit/webkittab.py | 9 +-------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 1f2be1476..9ad233c68 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -26,7 +26,7 @@ import typing import attr from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt, QEvent, QPoint) -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtWidgets import QWidget, QApplication, QDialog from PyQt5.QtPrintSupport import QPrintDialog, QPrinter from PyQt5.QtNetwork import QNetworkAccessManager @@ -1041,7 +1041,11 @@ class AbstractTab(QWidget): key: Qt.Key, modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: """Send a fake key event to this tab.""" - raise NotImplementedError + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) def dump_async(self, callback: typing.Callable[[str], None], *, diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 63f1201da..e04a28652 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -26,7 +26,7 @@ import html as html_utils from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, QUrl, QTimer, QObject) -from PyQt5.QtGui import QKeyEvent, QIcon +from PyQt5.QtGui import QIcon from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript @@ -1193,13 +1193,6 @@ class WebEngineTab(browsertab.AbstractTab): # percent encoded content is 2 megabytes minus 30 bytes. self._widget.setHtml(html, base_url) - def key_press(self, key, modifier=Qt.NoModifier): - press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, - 0, 0, 0) - self.send_event(press_evt) - self.send_event(release_evt) - def _show_error_page(self, url, error): """Show an error page in the tab.""" log.misc.debug("Showing error page for {}".format(error)) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index dcac1a3cb..7f1b8783a 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -25,7 +25,7 @@ import xml.etree.ElementTree from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, QSize) -from PyQt5.QtGui import QKeyEvent, QIcon +from PyQt5.QtGui import QIcon from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings from PyQt5.QtPrintSupport import QPrinter @@ -755,13 +755,6 @@ class WebKitTab(browsertab.AbstractTab): def title(self): return self._widget.title() - def key_press(self, key, modifier=Qt.NoModifier): - press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, - 0, 0, 0) - self.send_event(press_evt) - self.send_event(release_evt) - @pyqtSlot() def _on_history_trigger(self): url = self.url() From 609b8f1cee6ee5229aee81deced1dc8b43314ba2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:10:47 +0100 Subject: [PATCH 103/258] Fix return values of AbstractPrinting.check_*_support --- qutebrowser/browser/browsertab.py | 6 +++--- qutebrowser/browser/webengine/webenginetab.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 9ad233c68..bed403bfb 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -205,7 +205,7 @@ class AbstractPrinting: self._widget = None self._tab = tab - def check_pdf_support(self) -> bool: + def check_pdf_support(self) -> None: """Check whether writing to PDFs is supported. If it's not supported (by the current Qt version), a WebTabError is @@ -213,7 +213,7 @@ class AbstractPrinting: """ raise NotImplementedError - def check_printer_support(self) -> bool: + def check_printer_support(self) -> None: """Check whether writing to a printer is supported. If it's not supported (by the current Qt version), a WebTabError is @@ -221,7 +221,7 @@ class AbstractPrinting: """ raise NotImplementedError - def check_preview_support(self) -> bool: + def check_preview_support(self) -> None: """Check whether showing a print preview is supported. If it's not supported (by the current Qt version), a WebTabError is diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index e04a28652..4d8db5537 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -132,7 +132,7 @@ class WebEnginePrinting(browsertab.AbstractPrinting): """QtWebEngine implementations related to printing.""" def check_pdf_support(self): - return True + pass def check_printer_support(self): if not hasattr(self._widget.page(), 'print'): From cd8e3094b9f1df9e5d932d8ba3c1fd7bb5d3ccb0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:22:28 +0100 Subject: [PATCH 104/258] Avoid circular import --- qutebrowser/browser/browsertab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index bed403bfb..4c26f91a9 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -40,12 +40,13 @@ from qutebrowser.config import config from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, urlutils, message) from qutebrowser.misc import miscwidgets, objects -from qutebrowser.browser import mouse, hints, webelem +from qutebrowser.browser import mouse, hints from qutebrowser.qt import sip MYPY = False if MYPY: # pylint can't interpret type comments with Python 3.7 # pylint: disable=unused-import,useless-suppression + from qutebrowser.browser import webelem from qutebrowser.browser.inspector import AbstractWebInspector @@ -656,9 +657,9 @@ class AbstractElements: """Finding and handling of elements on the page.""" _MultiCallback = typing.Callable[ - [typing.Sequence[webelem.AbstractWebElement]], None] + [typing.Sequence['webelem.AbstractWebElement']], None] _SingleCallback = typing.Callable[ - [typing.Optional[webelem.AbstractWebElement]], None] + [typing.Optional['webelem.AbstractWebElement']], None] def __init__(self, tab: 'AbstractTab') -> None: self._widget = None @@ -768,7 +769,7 @@ class AbstractTabPrivate: if cur_mode == usertypes.KeyMode.insert: return - def _auto_insert_mode_cb(elem: webelem.AbstractWebElement) -> None: + def _auto_insert_mode_cb(elem: 'webelem.AbstractWebElement') -> None: """Called from JS after finding the focused element.""" if elem is None: log.webview.debug("No focused element!") From 73e196ff65106690d235c1e69a90ec5503776b35 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:23:14 +0100 Subject: [PATCH 105/258] Make url(requested=True) a keyword-only arg --- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 2 +- tests/helpers/stubs.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 4c26f91a9..ed07bd27c 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1015,7 +1015,7 @@ class AbstractTab(QWidget): self._progress = perc self.load_progress.emit(perc) - def url(self, requested: bool = False) -> QUrl: + def url(self, *, requested: bool = False) -> QUrl: raise NotImplementedError def progress(self) -> int: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 4d8db5537..ecbbd0ab0 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1139,7 +1139,7 @@ class WebEngineTab(browsertab.AbstractTab): self._openurl_prepare(url, predict=predict) self._widget.load(url) - def url(self, requested=False): + def url(self, *, requested=False): page = self._widget.page() if requested: return page.requestedUrl() diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 4abd58baf..0c2119132 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -268,7 +268,7 @@ class FakeWebTab(browsertab.AbstractTab): wrapped = QWidget() self._layout.wrap(self, wrapped) - def url(self, requested=False): + def url(self, *, requested=False): assert not requested return self._url From b2796d4a85920f9c7463c4f7c2ec6d1f2ce5e0c4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:26:05 +0100 Subject: [PATCH 106/258] Rename some AbstractZoom methods --- qutebrowser/browser/browsertab.py | 10 +++++----- qutebrowser/browser/commands.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ed07bd27c..2fb247a39 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -381,7 +381,7 @@ class AbstractZoom(QObject): levels, mode=usertypes.NeighborList.Modes.edge) self._neighborlist.fuzzyval = config.val.zoom.default - def offset(self, offset: int) -> None: + def apply_offset(self, offset: int) -> None: """Increase/Decrease the zoom level by the given offset. Args: @@ -418,10 +418,10 @@ class AbstractZoom(QObject): def factor(self) -> float: return self._zoom_factor - def set_default(self) -> None: + def apply_default(self) -> None: self._set_factor_internal(float(config.val.zoom.default) / 100) - def set_current(self) -> None: + def reapply(self) -> None: self._set_factor_internal(self._zoom_factor) @@ -888,7 +888,7 @@ class AbstractTab(QWidget): self.settings._settings = widget.settings() self._install_event_filter() - self.zoom.set_default() + self.zoom.apply_default() def _install_event_filter(self) -> None: raise NotImplementedError @@ -1003,7 +1003,7 @@ class AbstractTab(QWidget): if not self.title(): self.title_changed.emit(self.url().toDisplayString()) - self.zoom.set_current() + self.zoom.reapply() @pyqtSlot() def _on_history_trigger(self) -> None: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9ad5e0011..56a951a9f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -901,7 +901,7 @@ class CommandDispatcher: """ tab = self._current_widget() try: - perc = tab.zoom.offset(count) + perc = tab.zoom.apply_offset(count) except ValueError as e: raise cmdutils.CommandError(e) if not quiet: @@ -918,7 +918,7 @@ class CommandDispatcher: """ tab = self._current_widget() try: - perc = tab.zoom.offset(-count) + perc = tab.zoom.apply_offset(-count) except ValueError as e: raise cmdutils.CommandError(e) if not quiet: From 7a3d1b021282bab24d010a7d10f62051fbffee46 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:28:11 +0100 Subject: [PATCH 107/258] AbstractHistory: Check count --- qutebrowser/browser/browsertab.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 2fb247a39..ad5f42ab2 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -606,11 +606,17 @@ class AbstractHistory: def __iter__(self) -> typing.Iterable: raise NotImplementedError + def _check_count(self, count: int) -> None: + """Check whether the count is positive.""" + if count < 0: + raise WebTabError("count needs to be positive!") + def current_idx(self) -> int: raise NotImplementedError def back(self, count: int = 1) -> None: """Go back in the tab's history.""" + self._check_count(count) idx = self.current_idx() - count if idx >= 0: self._go_to_item(self._item_at(idx)) @@ -620,6 +626,7 @@ class AbstractHistory: def forward(self, count: int = 1) -> None: """Go forward in the tab's history.""" + self._check_count(count) idx = self.current_idx() + count if idx < len(self): self._go_to_item(self._item_at(idx)) From 2cd2c60a8b02a0fb2093003bfea14e1771f28112 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:30:28 +0100 Subject: [PATCH 108/258] Rename key_press to fake_key_press --- qutebrowser/browser/browsertab.py | 10 +++++----- qutebrowser/browser/webengine/webenginetab.py | 6 +++--- qutebrowser/browser/webkit/webkittab.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ad5f42ab2..3b39e52d1 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -512,9 +512,9 @@ class AbstractCaret(QObject): def _follow_enter(self, tab: bool) -> None: """Follow a link by faking an enter press.""" if tab: - self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) + self._tab.fake_key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) else: - self._tab.key_press(Qt.Key_Enter) + self._tab.fake_key_press(Qt.Key_Enter) def follow_selected(self, *, tab: bool = False) -> None: raise NotImplementedError @@ -1045,9 +1045,9 @@ class AbstractTab(QWidget): def stop(self) -> None: raise NotImplementedError - def key_press(self, - key: Qt.Key, - modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: + def fake_key_press(self, + key: Qt.Key, + modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: """Send a fake key event to this tab.""" press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index ecbbd0ab0..453b36974 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -423,7 +423,7 @@ class WebEngineScroller(browsertab.AbstractScroller): def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): """Send count fake key presses to this scroller's WebEngineTab.""" for _ in range(min(count, 1000)): - self._tab.key_press(key, modifier) + self._tab.fake_key_press(key, modifier) @pyqtSlot(QPointF) def _update_pos(self, pos): @@ -500,10 +500,10 @@ class WebEngineScroller(browsertab.AbstractScroller): self._repeated_key_press(Qt.Key_Right, count) def top(self): - self._tab.key_press(Qt.Key_Home) + self._tab.fake_key_press(Qt.Key_Home) def bottom(self): - self._tab.key_press(Qt.Key_End) + self._tab.fake_key_press(Qt.Key_End) def page_up(self, count=1): self._repeated_key_press(Qt.Key_PageUp, count) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 7f1b8783a..2c7083c25 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -474,7 +474,7 @@ class WebKitScroller(browsertab.AbstractScroller): if (getter is not None and frame.scrollBarValue(direction) == getter(direction)): return - self._tab.key_press(key) + self._tab.fake_key_press(key) def up(self, count=1): self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical) From a6d3a935d3595a6965985fa9d0ce5301095748bc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:38:12 +0100 Subject: [PATCH 109/258] Use an enum for IgnoreCase --- doc/help/settings.asciidoc | 3 ++- qutebrowser/browser/browsertab.py | 12 +++++----- qutebrowser/browser/webengine/webenginetab.py | 4 ++-- qutebrowser/browser/webkit/webkittab.py | 4 ++-- qutebrowser/config/configdata.yml | 7 +----- qutebrowser/config/configtypes.py | 23 ++++++++++++++++++- qutebrowser/utils/usertypes.py | 3 +++ 7 files changed, 38 insertions(+), 18 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index b7cb0349b..7da6e543e 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -2960,7 +2960,7 @@ Default: +pass:[false]+ === search.ignore_case When to find text on a page case-insensitively. -Type: <> +Type: <> Valid values: @@ -3624,6 +3624,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v |FontFamily|A Qt font family. |FormatString|A string with placeholders. |FuzzyUrl|A URL which gets interpreted as search if needed. +|IgnoreCase|Whether to search case insensitively. |Int|Base class for an integer setting. |Key|A name of a key. |List|A list of values. diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 3b39e52d1..0625b218e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -297,7 +297,7 @@ class AbstractSearch(QObject): self.text = None # type: typing.Optional[str] self.search_displayed = False - def _is_case_sensitive(self, ignore_case: str) -> bool: + def _is_case_sensitive(self, ignore_case: usertypes.IgnoreCase) -> bool: """Check if case-sensitivity should be used. This assumes self.text is already set properly. @@ -307,21 +307,21 @@ class AbstractSearch(QObject): """ assert self.text is not None mapping = { - 'smart': not self.text.islower(), - 'never': True, - 'always': False, + usertypes.IgnoreCase.smart: not self.text.islower(), + usertypes.IgnoreCase.never: True, + usertypes.IgnoreCase.always: False, } return mapping[ignore_case] def search(self, text: str, *, - ignore_case: str = 'never', + ignore_case: usertypes.IgnoreCase = usertypes.IgnoreCase.never, reverse: bool = False, result_cb: _Callback = None) -> None: """Find the given text on the page. Args: text: The text to search for. - ignore_case: Search case-insensitively. ('always'/'never/'smart') + ignore_case: Search case-insensitively. reverse: Reverse search direction. result_cb: Called with a bool indicating whether a match was found. """ diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 453b36974..fd380abae 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -205,8 +205,8 @@ class WebEngineSearch(browsertab.AbstractSearch): self._widget.findText(text, flags, wrapped_callback) - def search(self, text, *, ignore_case='never', reverse=False, - result_cb=None): + def search(self, text, *, ignore_case=usertypes.IgnoreCase.never, + reverse=False, result_cb=None): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 2c7083c25..44dc76f9d 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -125,8 +125,8 @@ class WebKitSearch(browsertab.AbstractSearch): self._widget.findText('') self._widget.findText('', QWebPage.HighlightAllOccurrences) - def search(self, text, *, ignore_case='never', reverse=False, - result_cb=None): + def search(self, text, *, ignore_case=usertypes.IgnoreCase.never, + reverse=False, result_cb=None): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index ff43cc156..bed4d9659 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -39,12 +39,7 @@ ignore_case: renamed: search.ignore_case search.ignore_case: - type: - name: String - valid_values: - - always: Search case-insensitively. - - never: Search case-sensitively. - - smart: Search case-sensitively if there are capital characters. + type: IgnoreCase default: smart desc: When to find text on a page case-insensitively. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 691584801..c0f3bec9f 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -62,7 +62,8 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar from qutebrowser.misc import objects from qutebrowser.config import configexc, configutils -from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch +from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch, + usertypes) from qutebrowser.keyinput import keyutils @@ -911,6 +912,26 @@ class ColorSystem(MappingType): } +class IgnoreCase(MappingType): + + """Whether to search case insensitively.""" + + def __init__(self, none_ok=False): + super().__init__( + none_ok, + valid_values=ValidValues( + ('always', "Search case-insensitively."), + ('never', "Search case-sensitively."), + ('smart', ("Search case-sensitively if there are capital " + "characters.")))), + + MAPPING = { + 'always': usertypes.IgnoreCase.always, + 'never': usertypes.IgnoreCase.never, + 'smart': usertypes.IgnoreCase.smart, + } + + class QtColor(BaseType): """A color value. diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index cd36db49a..5bb8d3aa9 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -253,6 +253,9 @@ JsLogLevel = enum.Enum('JsLogLevel', ['unknown', 'info', 'warning', 'error']) MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info']) +IgnoreCase = enum.Enum('IgnoreCase', ['smart', 'never', 'always']) + + class Question(QObject): """A question asked to the user, e.g. via the status bar. From 0104490978f6ad42617ece83b001f55fd804267a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 19:44:48 +0100 Subject: [PATCH 110/258] Use enum values for load_status_changed --- qutebrowser/browser/browsertab.py | 4 ++-- qutebrowser/mainwindow/statusbar/url.py | 11 +++++------ qutebrowser/mainwindow/tabbedbrowser.py | 2 +- tests/unit/mainwindow/statusbar/test_url.py | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 0625b218e..a53828af3 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -837,7 +837,7 @@ class AbstractTab(QWidget): load_finished = pyqtSignal(bool) icon_changed = pyqtSignal(QIcon) title_changed = pyqtSignal(str) - load_status_changed = pyqtSignal(str) + load_status_changed = pyqtSignal(usertypes.LoadStatus) new_tab_requested = pyqtSignal(QUrl) url_changed = pyqtSignal(QUrl) shutting_down = pyqtSignal() @@ -906,7 +906,7 @@ class AbstractTab(QWidget): raise TypeError("Type {} is no LoadStatus member!".format(val)) log.webview.debug("load status for {}: {}".format(repr(self), val)) self._load_status = val - self.load_status_changed.emit(val.name) + self.load_status_changed.emit(val) def send_event(self, evt: QEvent) -> None: """Send the given event to the underlying widget. diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index fda09d642..8ac258465 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -112,19 +112,18 @@ class UrlText(textbase.TextBase): self._urltype = UrlType.normal config.set_register_stylesheet(self, update=False) - @pyqtSlot(str) - def on_load_status_changed(self, status_str): + @pyqtSlot(usertypes.LoadStatus) + def on_load_status_changed(self, status): """Slot for load_status_changed. Sets URL color accordingly. Args: - status_str: The LoadStatus as string. + status: The usertypes.LoadStatus. """ - status = usertypes.LoadStatus[status_str] if status in [usertypes.LoadStatus.success, usertypes.LoadStatus.success_https, usertypes.LoadStatus.error, usertypes.LoadStatus.warn]: - self._normal_url_type = UrlType[status_str] + self._normal_url_type = UrlType[status.name] else: self._normal_url_type = UrlType.normal self._update_url() @@ -172,5 +171,5 @@ class UrlText(textbase.TextBase): self._normal_url = urlutils.safe_display_string(tab.url()) else: self._normal_url = '' - self.on_load_status_changed(tab.load_status().name) + self.on_load_status_changed(tab.load_status()) self._update_url() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d00d5d797..5530b0e37 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -102,7 +102,7 @@ class TabbedBrowser(QWidget): cur_url_changed = pyqtSignal(QUrl) cur_link_hovered = pyqtSignal(str) cur_scroll_perc_changed = pyqtSignal(int, int) - cur_load_status_changed = pyqtSignal(str) + cur_load_status_changed = pyqtSignal(usertypes.LoadStatus) cur_fullscreen_requested = pyqtSignal(bool) cur_caret_selection_toggled = pyqtSignal(bool) close_window = pyqtSignal() diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index bda51b504..801a71f0a 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -89,7 +89,7 @@ def test_set_url(url_widget, url_text, expected, which): def test_on_load_status_changed(url_widget, status, expected): """Test text when status is changed.""" url_widget.set_url(QUrl('www.example.com')) - url_widget.on_load_status_changed(status.name) + url_widget.on_load_status_changed(status) assert url_widget._urltype == expected @@ -139,7 +139,7 @@ def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl): ]) def test_normal_url(url_widget, qurl, load_status, expected_status): url_widget.set_url(qurl) - url_widget.on_load_status_changed(load_status.name) + url_widget.on_load_status_changed(load_status) url_widget.set_hover_url(qurl.toDisplayString()) url_widget.set_hover_url("") assert url_widget.text() == qurl.toDisplayString() From 27ee3280b2a04db3fc8b6a1b661aa69a6fb90872 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 29 Nov 2018 20:29:43 +0100 Subject: [PATCH 111/258] Separate success/error callbacks --- qutebrowser/browser/browsertab.py | 4 +++- qutebrowser/browser/hints.py | 14 +++++--------- qutebrowser/browser/navigate.py | 10 ++-------- qutebrowser/browser/webengine/webenginetab.py | 14 ++++++++------ qutebrowser/browser/webkit/webkittab.py | 6 ++++-- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index a53828af3..175c1e60d 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -667,13 +667,15 @@ class AbstractElements: [typing.Sequence['webelem.AbstractWebElement']], None] _SingleCallback = typing.Callable[ [typing.Optional['webelem.AbstractWebElement']], None] + _ErrorCallback = typing.Callable[[Exception], None] def __init__(self, tab: 'AbstractTab') -> None: self._widget = None self._tab = tab def find_css(self, selector: str, - callback: _MultiCallback, *, + callback: _MultiCallback, + error_callback: _ErrorCallback, *, only_visible: bool = False) -> None: """Find all HTML elements matching a given selector async. diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 230437b40..31fd9b6a2 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -597,13 +597,6 @@ class HintManager(QObject): log.hints.debug("In _start_cb without context!") return - if elems is None: - message.error("Unknown error while getting hint elements.") - return - elif isinstance(elems, webelem.Error): - message.error(str(elems)) - return - if not elems: message.error("No elements found.") return @@ -747,8 +740,11 @@ class HintManager(QObject): except webelem.Error as e: raise cmdutils.CommandError(str(e)) - self._context.tab.elements.find_css(selector, self._start_cb, - only_visible=True) + self._context.tab.elements.find_css( + selector, + callback=self._start_cb, + error_cb=lambda err: message.error(str(err)), + only_visible=True) def _get_hint_mode(self, mode): """Get the hinting mode to use based on a mode argument.""" diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 0f80775bd..515b023ae 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -116,13 +116,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, window: True to open in a new window, False for the current one. """ def _prevnext_cb(elems): - if elems is None: - message.error("Unknown error while getting hint elements") - return - elif isinstance(elems, webelem.Error): - message.error(str(elems)) - return - elem = _find_prevnext(prev, elems) word = 'prev' if prev else 'forward' @@ -155,4 +148,5 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, except webelem.Error as e: raise Error(str(e)) - browsertab.elements.find_css(link_selector, _prevnext_cb) + browsertab.elements.find_css(link_selector, callback=_prevnext_cb, + error_cb=lambda err: message.error(str(err))) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index fd380abae..f167b6b8c 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -591,19 +591,20 @@ class WebEngineElements(browsertab.AbstractElements): """QtWebEngine implemementations related to elements on the page.""" - def _js_cb_multiple(self, callback, js_elems): + def _js_cb_multiple(self, callback, error_cb, js_elems): """Handle found elements coming from JS and call the real callback. Args: callback: The callback to call with the found elements. - Called with None if there was an error. + error_cb: The callback to call in case of an error. js_elems: The elements serialized from javascript. """ if js_elems is None: - callback(None) + error_cb(webelem.Error("Unknown error while getting " + "elements")) return elif not js_elems['success']: - callback(webelem.Error(js_elems['error'])) + error_cb(webelem.Error(js_elems['error'])) return elems = [] @@ -630,10 +631,11 @@ class WebEngineElements(browsertab.AbstractElements): elem = webengineelem.WebEngineElement(js_elem, tab=self._tab) callback(elem) - def find_css(self, selector, callback, *, only_visible=False): + def find_css(self, selector, callback, error_cb, *, + only_visible=False): js_code = javascript.assemble('webelem', 'find_css', selector, only_visible) - js_cb = functools.partial(self._js_cb_multiple, callback) + js_cb = functools.partial(self._js_cb_multiple, callback, error_cb) self._tab.run_js_async(js_code, js_cb) def find_id(self, elem_id, callback): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 44dc76f9d..5a989e81e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -563,7 +563,8 @@ class WebKitElements(browsertab.AbstractElements): """QtWebKit implemementations related to elements on the page.""" - def find_css(self, selector, callback, *, only_visible=False): + def find_css(self, selector, callback, error_cb, *, only_visible=False): + utils.unused(error_cb) mainframe = self._widget.page().mainFrame() if mainframe is None: raise browsertab.WebTabError("No frame focused!") @@ -592,7 +593,8 @@ class WebKitElements(browsertab.AbstractElements): # Escape non-alphanumeric characters in the selector # https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id) - self.find_css('#' + elem_id, find_id_cb) + self.find_css('#' + elem_id, find_id_cb, + error_callback=lambda exc: None) def find_focused(self, callback): frame = self._widget.page().currentFrame() From 5bf0dffa9558b7f0da5ba763bb87c5f380ff0261 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:23:52 +0100 Subject: [PATCH 112/258] Move history (de)serializing to a private_api object --- qutebrowser/browser/browsertab.py | 35 ++++++++---- qutebrowser/browser/commands.py | 4 +- qutebrowser/browser/webengine/webenginetab.py | 57 +++++++++++-------- qutebrowser/browser/webkit/webkittab.py | 57 +++++++++++-------- qutebrowser/mainwindow/tabbedbrowser.py | 4 +- qutebrowser/misc/sessions.py | 2 +- 6 files changed, 92 insertions(+), 67 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 175c1e60d..311843964 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -592,6 +592,27 @@ class AbstractScroller(QObject): raise NotImplementedError +class AbstractHistoryPrivate: + + """Private API related to the history.""" + + def __init__(self, tab: 'AbstractTab'): + self._tab = tab + self._history = None + + def serialize(self) -> bytes: + """Serialize into an opaque format understood by self.deserialize.""" + raise NotImplementedError + + def deserialize(self, data: bytes) -> None: + """Deserialize from a format produced by self.serialize.""" + raise NotImplementedError + + def load_items(self, items: typing.Sequence) -> None: + """Deserialize from a list of WebHistoryItems.""" + raise NotImplementedError + + class AbstractHistory: """The history attribute of a AbstractTab.""" @@ -599,6 +620,7 @@ class AbstractHistory: def __init__(self, tab: 'AbstractTab') -> None: self._tab = tab self._history = None + self.private_api = AbstractHistoryPrivate(tab) def __len__(self) -> int: raise NotImplementedError @@ -646,18 +668,6 @@ class AbstractHistory: def _go_to_item(self, item: typing.Any) -> None: raise NotImplementedError - def serialize(self) -> bytes: - """Serialize into an opaque format understood by self.deserialize.""" - raise NotImplementedError - - def deserialize(self, data: bytes) -> None: - """Deserialize from a format produced by self.serialize.""" - raise NotImplementedError - - def load_items(self, items: typing.Sequence) -> None: - """Deserialize from a list of WebHistoryItems.""" - raise NotImplementedError - class AbstractElements: @@ -885,6 +895,7 @@ class AbstractTab(QWidget): self._widget = widget self._layout.wrap(self, widget) self.history._history = widget.history() + self.history.private_api._history = widget.history() self.scroller._init_widget(widget) self.caret._widget = widget self.zoom._widget = widget diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 56a951a9f..01542cdea 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -463,7 +463,7 @@ class CommandDispatcher: cur_title = self._tabbed_browser.widget.page_title( self._current_index()) try: - history = curtab.history.serialize() + history = curtab.history.private_api.serialize() except browsertab.WebTabError as e: raise cmdutils.CommandError(e) @@ -486,7 +486,7 @@ class CommandDispatcher: new_tabbed_browser.widget.window().setWindowIcon(curtab.icon()) newtab.data.keep_icon = True - newtab.history.deserialize(history) + newtab.history.private_api.deserialize(history) newtab.zoom.set_factor(curtab.zoom.factor()) new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned) return newtab diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index f167b6b8c..fbd168fee 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -518,31 +518,7 @@ class WebEngineScroller(browsertab.AbstractScroller): return self._at_bottom -class WebEngineHistory(browsertab.AbstractHistory): - - """QtWebEngine implementations related to page history.""" - - def __len__(self): - return len(self._history) - - def __iter__(self): - return iter(self._history.items()) - - def current_idx(self): - return self._history.currentItemIndex() - - def can_go_back(self): - return self._history.canGoBack() - - def can_go_forward(self): - return self._history.canGoForward() - - def _item_at(self, i): - return self._history.itemAt(i) - - def _go_to_item(self, item): - self._tab.predicted_navigation.emit(item.url()) - self._history.goToItem(item) +class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate): def serialize(self): if not qtutils.version_check('5.9', compiled=False): @@ -579,6 +555,37 @@ class WebEngineHistory(browsertab.AbstractHistory): self._tab.load_finished.connect(_on_load_finished) +class WebEngineHistory(browsertab.AbstractHistory): + + """QtWebEngine implementations related to page history.""" + + def __init__(self, tab): + super().__init__(tab) + self.private_api = WebEngineHistoryPrivate(tab) + + def __len__(self): + return len(self._history) + + def __iter__(self): + return iter(self._history.items()) + + def current_idx(self): + return self._history.currentItemIndex() + + def can_go_back(self): + return self._history.canGoBack() + + def can_go_forward(self): + return self._history.canGoForward() + + def _item_at(self, i): + return self._history.itemAt(i) + + def _go_to_item(self, item): + self._tab.predicted_navigation.emit(item.url()) + self._history.goToItem(item) + + class WebEngineZoom(browsertab.AbstractZoom): """QtWebEngine implementations related to zooming.""" diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 5a989e81e..df8abe254 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -509,31 +509,7 @@ class WebKitScroller(browsertab.AbstractScroller): return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical) -class WebKitHistory(browsertab.AbstractHistory): - - """QtWebKit implementations related to page history.""" - - def __len__(self): - return len(self._history) - - def __iter__(self): - return iter(self._history.items()) - - def current_idx(self): - return self._history.currentItemIndex() - - def can_go_back(self): - return self._history.canGoBack() - - def can_go_forward(self): - return self._history.canGoForward() - - def _item_at(self, i): - return self._history.itemAt(i) - - def _go_to_item(self, item): - self._tab.predicted_navigation.emit(item.url()) - self._history.goToItem(item) +class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate): def serialize(self): return qtutils.serialize(self._history) @@ -559,6 +535,37 @@ class WebKitHistory(browsertab.AbstractHistory): self._tab.scroller.to_point, cur_data['scroll-pos'])) +class WebKitHistory(browsertab.AbstractHistory): + + """QtWebKit implementations related to page history.""" + + def __init__(self, tab): + super().__init__(tab) + self.private_api = WebKitHistoryPrivate(tab) + + def __len__(self): + return len(self._history) + + def __iter__(self): + return iter(self._history.items()) + + def current_idx(self): + return self._history.currentItemIndex() + + def can_go_back(self): + return self._history.canGoBack() + + def can_go_forward(self): + return self._history.canGoForward() + + def _item_at(self, i): + return self._history.itemAt(i) + + def _go_to_item(self, item): + self._tab.predicted_navigation.emit(item.url()) + self._history.goToItem(item) + + class WebKitElements(browsertab.AbstractElements): """QtWebKit implemementations related to elements on the page.""" diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 5530b0e37..07a1122d3 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -345,7 +345,7 @@ class TabbedBrowser(QWidget): urlutils.invalid_url_error(tab.url(), "saving tab") elif add_undo: try: - history_data = tab.history.serialize() + history_data = tab.history.private_api.serialize() except browsertab.WebTabError: pass # special URL else: @@ -391,7 +391,7 @@ class TabbedBrowser(QWidget): else: newtab = self.tabopen(background=False, idx=entry.index) - newtab.history.deserialize(entry.history) + newtab.history.private_api.deserialize(entry.history) self.widget.set_tab_pinned(newtab, entry.pinned) @pyqtSlot('QUrl', bool) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 4f80ad2b0..357d5be64 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -401,7 +401,7 @@ class SessionManager(QObject): new_tab.title_changed.emit(histentry['title']) try: - new_tab.history.load_items(entries) + new_tab.history.private_api.load_items(entries) except ValueError as e: raise SessionError(e) From f6c36ccbee9328d261799f1c09e1341444a1e618 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:31:03 +0100 Subject: [PATCH 113/258] Rename openurl to load_url We still call the :open command openurl, but in the tab API and in TabbedBrowser it's now called load_url. --- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/browser/commands.py | 6 +++--- qutebrowser/browser/navigate.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 8 ++++---- qutebrowser/browser/webkit/webkittab.py | 8 ++++---- qutebrowser/browser/webkit/webview.py | 8 -------- qutebrowser/config/configcommands.py | 4 ++-- qutebrowser/mainwindow/tabbedbrowser.py | 14 +++++++------- qutebrowser/misc/utilcmds.py | 4 ++-- tests/helpers/stubs.py | 8 ++++---- tests/unit/browser/test_caret.py | 2 +- tests/unit/browser/test_hints.py | 4 ++-- tests/unit/commands/test_argparser.py | 2 +- tests/unit/config/test_configcommands.py | 4 ++-- tests/unit/javascript/conftest.py | 2 +- tests/unit/misc/test_utilcmds.py | 2 +- 16 files changed, 36 insertions(+), 44 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 311843964..3f71fa074 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1049,7 +1049,7 @@ class AbstractTab(QWidget): if predict: self.predicted_navigation.emit(url) - def openurl(self, url: QUrl, *, predict: bool = True) -> None: + def load_url(self, url: QUrl, *, predict: bool = True) -> None: raise NotImplementedError def reload(self, *, force: bool = False) -> None: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 01542cdea..63c2da3b3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -138,7 +138,7 @@ class CommandDispatcher: tabbed_browser.tabopen(url, background=True, related=related) else: widget = self._current_widget() - widget.openurl(url) + widget.load_url(url) def _cntwidget(self, count=None): """Return a widget based on a count/idx. @@ -321,7 +321,7 @@ class CommandDispatcher: elif curtab.data.pinned: message.info("Tab is pinned!") else: - curtab.openurl(cur_url) + curtab.load_url(cur_url) def _parse_url(self, url, *, force_search=False): """Parse a URL or quickmark or search query. @@ -1247,7 +1247,7 @@ class CommandDispatcher: if output: tb = objreg.get('tabbed-browser', scope='window', window='last-focused') - tb.openurl(QUrl('qute://spawn-output'), newtab=True) + tb.load_url(QUrl('qute://spawn-output'), newtab=True) if userscript: def _selection_callback(s): diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 515b023ae..240878ed6 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -141,7 +141,7 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, elif tab: cur_tabbed_browser.tabopen(url, background=background) else: - browsertab.openurl(url) + browsertab.load_url(url) try: link_selector = webelem.css_selector('links', baseurl) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index fbd168fee..29c26aca5 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -478,7 +478,7 @@ class WebEngineScroller(browsertab.AbstractScroller): def to_anchor(self, name): url = self._tab.url() url.setFragment(name) - self._tab.openurl(url) + self._tab.load_url(url) def delta(self, x=0, y=0): self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y)) @@ -1134,11 +1134,11 @@ class WebEngineTab(browsertab.AbstractTab): self.zoom.set_factor(self._saved_zoom) self._saved_zoom = None - def openurl(self, url, *, predict=True): - """Open the given URL in this tab. + def load_url(self, url, *, predict=True): + """Load the given URL in this tab. Arguments: - url: The QUrl to open. + url: The QUrl to load. predict: If set to False, predicted_navigation is not emitted. """ if sip.isdeleted(self._widget): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index df8abe254..d92c60609 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -391,7 +391,7 @@ class WebKitCaret(browsertab.AbstractCaret): if tab: self._tab.new_tab_requested.emit(url) else: - self._tab.openurl(url) + self._tab.load_url(url) def follow_selected(self, *, tab=False): try: @@ -722,9 +722,9 @@ class WebKitTab(browsertab.AbstractTab): settings = widget.settings() settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) - def openurl(self, url, *, predict=True): + def load_url(self, url, *, predict=True): self._openurl_prepare(url, predict=predict) - self._widget.openurl(url) + self._widget.load(url) def url(self, requested=False): frame = self._widget.page().mainFrame() @@ -829,7 +829,7 @@ class WebKitTab(browsertab.AbstractTab): if (navigation.navigation_type == navigation.Type.link_clicked and target != usertypes.ClickTarget.normal): tab = shared.get_tab(self.win_id, target) - tab.openurl(navigation.url) + tab.load_url(navigation.url) self.data.open_target = usertypes.ClickTarget.normal navigation.accepted = False diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 3d56366c3..8921e211c 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -118,14 +118,6 @@ class WebView(QWebView): self.stop() self.page().shutdown() - def openurl(self, url): - """Open a URL in the browser. - - Args: - url: The URL to load as QUrl - """ - self.load(url) - def createWindow(self, wintype): """Called by Qt when a page wants to create a new window. diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 8587acc3f..74a381507 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -101,7 +101,7 @@ class ConfigCommands: if option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) + tabbed_browser.load_url(QUrl('qute://settings'), newtab=False) return if option.endswith('!'): @@ -147,7 +147,7 @@ class ConfigCommands: if key is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True) + tabbed_browser.load_url(QUrl('qute://bindings'), newtab=True) return seq = self._parse_key(key) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 07a1122d3..d33b13795 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -303,12 +303,12 @@ class TabbedBrowser(QWidget): if last_close == 'close': self.close_window.emit() elif last_close == 'blank': - self.openurl(QUrl('about:blank'), newtab=True) + self.load_url(QUrl('about:blank'), newtab=True) elif last_close == 'startpage': for url in config.val.url.start_pages: - self.openurl(url, newtab=True) + self.load_url(url, newtab=True) elif last_close == 'default-page': - self.openurl(config.val.url.default_page, newtab=True) + self.load_url(config.val.url.default_page, newtab=True) def _remove_tab(self, tab, *, add_undo=True, new_undo=True, crashed=False): """Remove a tab from the tab list and delete it properly. @@ -395,7 +395,7 @@ class TabbedBrowser(QWidget): self.widget.set_tab_pinned(newtab, entry.pinned) @pyqtSlot('QUrl', bool) - def openurl(self, url, newtab): + def load_url(self, url, newtab): """Open a URL, used as a slot. Args: @@ -406,7 +406,7 @@ class TabbedBrowser(QWidget): if newtab or self.widget.currentWidget() is None: self.tabopen(url, background=False) else: - self.widget.currentWidget().openurl(url) + self.widget.currentWidget().load_url(url) @pyqtSlot(int) def on_tab_close_requested(self, idx): @@ -483,7 +483,7 @@ class TabbedBrowser(QWidget): self.widget.insertTab(idx, tab, "") if url is not None: - tab.openurl(url) + tab.load_url(url) if background is None: background = config.val.tabs.background @@ -879,7 +879,7 @@ class TabbedBrowser(QWidget): self.cur_load_finished.disconnect(callback) tab.scroller.to_point(point) - self.openurl(url, newtab=False) + self.load_url(url, newtab=False) self.cur_load_finished.connect(callback) else: message.error("Mark {} is not set".format(key)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index e78b6d9b7..761498e5f 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -271,7 +271,7 @@ def debug_pyeval(s, file=False, quiet=False): else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True) + tabbed_browser.load_url(QUrl('qute://pyeval'), newtab=True) @cmdutils.register(debug=True) @@ -386,7 +386,7 @@ def version(win_id, paste=False): """ tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute://version'), newtab=True) + tabbed_browser.load_url(QUrl('qute://version'), newtab=True) if paste: pastebin_version() diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 0c2119132..89330ab64 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -492,7 +492,7 @@ class TabbedBrowserStub(QObject): super().__init__(parent) self.widget = TabWidgetStub() self.shutting_down = False - self.opened_url = None + self.loaded_url = None self.cur_url = None def on_tab_close_requested(self, idx): @@ -502,10 +502,10 @@ class TabbedBrowserStub(QObject): return self.widget.tabs def tabopen(self, url): - self.opened_url = url + self.loaded_url = url - def openurl(self, url, *, newtab): - self.opened_url = url + def load_url(self, url, *, newtab): + self.loaded_url = url def current_url(self): if self.current_url is None: diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 6165546e5..b3706ffca 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -30,7 +30,7 @@ from qutebrowser.utils import usertypes, qtutils @pytest.fixture def caret(web_tab, qtbot, mode_manager): with qtbot.wait_signal(web_tab.load_finished): - web_tab.openurl(QUrl('qute://testdata/data/caret.html')) + web_tab.load_url(QUrl('qute://testdata/data/caret.html')) mode_manager.enter(usertypes.KeyMode.caret) diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py index 5c2758d84..609fb3dc6 100644 --- a/tests/unit/browser/test_hints.py +++ b/tests/unit/browser/test_hints.py @@ -56,7 +56,7 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge, tab = tabbed_browser.widget.tabs[0] with qtbot.wait_signal(tab.load_finished): - tab.openurl(QUrl('qute://testdata/data/hints/benchmark.html')) + tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html')) manager = qutebrowser.browser.hints.HintManager(0, 0) @@ -76,7 +76,7 @@ def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge, tab = tabbed_browser.widget.tabs[0] with qtbot.wait_signal(tab.load_finished): - tab.openurl(QUrl('qute://testdata/data/hints/benchmark.html')) + tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html')) manager = qutebrowser.browser.hints.HintManager(0, 0) diff --git a/tests/unit/commands/test_argparser.py b/tests/unit/commands/test_argparser.py index 624306ded..0b6afc210 100644 --- a/tests/unit/commands/test_argparser.py +++ b/tests/unit/commands/test_argparser.py @@ -60,7 +60,7 @@ class TestArgumentParser: parser.parse_args(['--help']) expected_url = QUrl('qute://help/commands.html#foo') - assert tabbed_browser_stubs[1].opened_url == expected_url + assert tabbed_browser_stubs[1].loaded_url == expected_url @pytest.mark.parametrize('types, value, expected', [ diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 001d55899..209d8db44 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -59,7 +59,7 @@ class TestSet: Should open qute://settings.""" commands.set(win_id=0) - assert tabbed_browser_stubs[0].opened_url == QUrl('qute://settings') + assert tabbed_browser_stubs[0].loaded_url == QUrl('qute://settings') @pytest.mark.parametrize('option', ['url.auto_search?', 'url.auto_search']) def test_get(self, config_stub, commands, message_mock, option): @@ -620,7 +620,7 @@ class TestBind: config_stub.val.bindings.default = no_bindings config_stub.val.bindings.commands = no_bindings commands.bind(win_id=0) - assert tabbed_browser_stubs[0].opened_url == QUrl('qute://bindings') + assert tabbed_browser_stubs[0].loaded_url == QUrl('qute://bindings') @pytest.mark.parametrize('command', ['nop', 'nope']) def test_bind(self, commands, config_stub, no_bindings, key_config_stub, diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 2078513b4..486839237 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -86,7 +86,7 @@ class JSTester: """ with self.qtbot.waitSignal(self.tab.load_finished, timeout=2000) as blocker: - self.tab.openurl(url) + self.tab.load_url(url) if not force: assert blocker.args == [True] diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 73f97095f..b4af06aff 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -155,4 +155,4 @@ def tabbed_browser(stubs, win_registry): def test_version(tabbed_browser, qapp): utilcmds.version(win_id=0) - assert tabbed_browser.opened_url == QUrl('qute://version') + assert tabbed_browser.loaded_url == QUrl('qute://version') From 6a27cd4e1571e8e2b85e2baaf43099e31b2ce85b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:35:56 +0100 Subject: [PATCH 114/258] fixup --- qutebrowser/browser/webengine/webenginetab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 29c26aca5..aa5b37aa8 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1366,7 +1366,7 @@ class WebEngineTab(browsertab.AbstractTab): log.config.debug( "Loading {} again because of config change".format( self._reload_url.toDisplayString())) - QTimer.singleShot(100, functools.partial(self.openurl, + QTimer.singleShot(100, functools.partial(self.load_url, self._reload_url, predict=False)) self._reload_url = None From da5527f5eff6a5a7b090f5b0aa5130b6dce294fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:37:29 +0100 Subject: [PATCH 115/258] Rename predicted_navigation --- qutebrowser/browser/browsertab.py | 20 +++++++++------- qutebrowser/browser/webengine/webenginetab.py | 24 ++++++++++--------- qutebrowser/browser/webkit/webkittab.py | 9 +++---- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 3f71fa074..ffc6c4db1 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -839,7 +839,7 @@ class AbstractTab(QWidget): process terminated. arg 0: A TerminationStatus member. arg 1: The exit code. - predicted_navigation: Emitted before we tell Qt to open a URL. + before_load_started: Emitted before we tell Qt to open a URL. """ window_close_requested = pyqtSignal() @@ -857,7 +857,7 @@ class AbstractTab(QWidget): add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) - predicted_navigation = pyqtSignal(QUrl) + before_load_started = pyqtSignal(QUrl) def __init__(self, *, win_id: int, private: bool, parent: QWidget = None) -> None: @@ -888,7 +888,7 @@ class AbstractTab(QWidget): objreg.register('hintmanager', hintmanager, scope='tab', window=self.win_id, tab=self.tab_id) - self.predicted_navigation.connect(self._on_predicted_navigation) + self.before_load_started.connect(self._on_before_load_started) def _set_widget(self, widget: QWidget) -> None: # pylint: disable=protected-access @@ -943,11 +943,11 @@ class AbstractTab(QWidget): QApplication.postEvent(recipient, evt) @pyqtSlot(QUrl) - def _on_predicted_navigation(self, url: QUrl) -> None: + def _on_before_load_started(self, url: QUrl) -> None: """Adjust the title if we are going to visit a URL soon.""" qtutils.ensure_valid(url) url_string = url.toDisplayString() - log.webview.debug("Predicted navigation: {}".format(url_string)) + log.webview.debug("Going to start loading: {}".format(url_string)) self.title_changed.emit(url_string) @pyqtSlot(QUrl) @@ -1044,12 +1044,14 @@ class AbstractTab(QWidget): def load_status(self) -> usertypes.LoadStatus: return self._load_status - def _openurl_prepare(self, url: QUrl, *, predict: bool = True) -> None: + def _openurl_prepare(self, url: QUrl, *, + emit_before_load_started: bool = True) -> None: qtutils.ensure_valid(url) - if predict: - self.predicted_navigation.emit(url) + if emit_before_load_started: + self.before_load_started.emit(url) - def load_url(self, url: QUrl, *, predict: bool = True) -> None: + def load_url(self, url: QUrl, *, + emit_before_load_started: bool = True) -> None: raise NotImplementedError def reload(self, *, force: bool = False) -> None: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index aa5b37aa8..98e3c1402 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -537,7 +537,7 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate): def load_items(self, items): if items: - self._tab.predicted_navigation.emit(items[-1].url) + self._tab.before_load_started.emit(items[-1].url) stream, _data, cur_data = tabhistory.serialize(items) qtutils.deserialize_stream(stream, self._history) @@ -582,7 +582,7 @@ class WebEngineHistory(browsertab.AbstractHistory): return self._history.itemAt(i) def _go_to_item(self, item): - self._tab.predicted_navigation.emit(item.url()) + self._tab.before_load_started.emit(item.url()) self._history.goToItem(item) @@ -1134,18 +1134,20 @@ class WebEngineTab(browsertab.AbstractTab): self.zoom.set_factor(self._saved_zoom) self._saved_zoom = None - def load_url(self, url, *, predict=True): + def load_url(self, url, *, emit_before_load_started=True): """Load the given URL in this tab. Arguments: url: The QUrl to load. - predict: If set to False, predicted_navigation is not emitted. + emit_before_load_started: If set to False, before_load_started is + not emitted. """ if sip.isdeleted(self._widget): # https://github.com/qutebrowser/qutebrowser/issues/3896 return self._saved_zoom = self.zoom.factor() - self._openurl_prepare(url, predict=predict) + self._openurl_prepare( + url, emit_before_load_started=emit_before_load_started) self._widget.load(url) def url(self, *, requested=False): @@ -1366,9 +1368,9 @@ class WebEngineTab(browsertab.AbstractTab): log.config.debug( "Loading {} again because of config change".format( self._reload_url.toDisplayString())) - QTimer.singleShot(100, functools.partial(self.load_url, - self._reload_url, - predict=False)) + QTimer.singleShot(100, functools.partial( + self.load_url, self._reload_url, + emit_before_load_started=False)) self._reload_url = None if not qtutils.version_check('5.10', compiled=False): @@ -1407,12 +1409,12 @@ class WebEngineTab(browsertab.AbstractTab): self._show_error_page(url, str(error)) @pyqtSlot(QUrl) - def _on_predicted_navigation(self, url): + def _on_before_load_started(self, url): """If we know we're going to visit a URL soon, change the settings. This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 """ - super()._on_predicted_navigation(url) + super()._on_before_load_started(url) if not qtutils.version_check('5.11.1', compiled=False): self.settings.update_for_url(url) @@ -1490,7 +1492,7 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._restore_zoom) page.loadFinished.connect(self._on_load_finished) - self.predicted_navigation.connect(self._on_predicted_navigation) + self.before_load_started.connect(self._on_before_load_started) # pylint: disable=protected-access self.audio._connect_signals() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index d92c60609..3c853814a 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -519,7 +519,7 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate): def load_items(self, items): if items: - self._tab.predicted_navigation.emit(items[-1].url) + self._tab.before_load_started.emit(items[-1].url) stream, _data, user_data = tabhistory.serialize(items) qtutils.deserialize_stream(stream, self._history) @@ -562,7 +562,7 @@ class WebKitHistory(browsertab.AbstractHistory): return self._history.itemAt(i) def _go_to_item(self, item): - self._tab.predicted_navigation.emit(item.url()) + self._tab.before_load_started.emit(item.url()) self._history.goToItem(item) @@ -722,8 +722,9 @@ class WebKitTab(browsertab.AbstractTab): settings = widget.settings() settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) - def load_url(self, url, *, predict=True): - self._openurl_prepare(url, predict=predict) + def load_url(self, url, *, emit_before_load_started=True): + self._openurl_prepare( + url, emit_before_load_started=emit_before_load_started) self._widget.load(url) def url(self, requested=False): From a7f7607b4d6cac5670a04373aa505f9c15767a11 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:38:11 +0100 Subject: [PATCH 116/258] Rename _openurl_prepare --- qutebrowser/browser/browsertab.py | 4 ++-- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webkit/webkittab.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ffc6c4db1..5054f384f 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1044,8 +1044,8 @@ class AbstractTab(QWidget): def load_status(self) -> usertypes.LoadStatus: return self._load_status - def _openurl_prepare(self, url: QUrl, *, - emit_before_load_started: bool = True) -> None: + def _load_url_prepare(self, url: QUrl, *, + emit_before_load_started: bool = True) -> None: qtutils.ensure_valid(url) if emit_before_load_started: self.before_load_started.emit(url) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 98e3c1402..053c6f50e 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1146,7 +1146,7 @@ class WebEngineTab(browsertab.AbstractTab): # https://github.com/qutebrowser/qutebrowser/issues/3896 return self._saved_zoom = self.zoom.factor() - self._openurl_prepare( + self._load_url_prepare( url, emit_before_load_started=emit_before_load_started) self._widget.load(url) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 3c853814a..a934f600d 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -723,7 +723,7 @@ class WebKitTab(browsertab.AbstractTab): settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) def load_url(self, url, *, emit_before_load_started=True): - self._openurl_prepare( + self._load_url_prepare( url, emit_before_load_started=emit_before_load_started) self._widget.load(url) From 5199681b417e07bb46e9650a2d9833141dab8232 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:45:45 +0100 Subject: [PATCH 117/258] Rename add_history_item --- qutebrowser/browser/browsertab.py | 5 +++-- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webkit/webkittab.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 5054f384f..885d5d3f9 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -854,7 +854,8 @@ class AbstractTab(QWidget): url_changed = pyqtSignal(QUrl) shutting_down = pyqtSignal() contents_size_changed = pyqtSignal(QSizeF) - add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title + # url, requested url, title + history_item_triggered = pyqtSignal(QUrl, QUrl, str) fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) before_load_started = pyqtSignal(QUrl) @@ -1027,7 +1028,7 @@ class AbstractTab(QWidget): @pyqtSlot() def _on_history_trigger(self) -> None: - """Emit add_history_item when triggered by backend-specific signal.""" + """Emit history_item_triggered based on backend-specific signal.""" raise NotImplementedError @pyqtSlot(int) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 053c6f50e..cf1649ed6 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1240,7 +1240,7 @@ class WebEngineTab(browsertab.AbstractTab): log.misc.debug("Ignoring invalid URL being added to history") return - self.add_history_item.emit(url, requested_url, title) + self.history_item_triggered.emit(url, requested_url, title) @pyqtSlot(QUrl, 'QAuthenticator*', 'QString') def _on_proxy_authentication_required(self, url, authenticator, diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index a934f600d..d0ff650b5 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -769,7 +769,7 @@ class WebKitTab(browsertab.AbstractTab): def _on_history_trigger(self): url = self.url() requested_url = self.url(requested=True) - self.add_history_item.emit(url, requested_url, self.title()) + self.history_item_triggered.emit(url, requested_url, self.title()) def set_html(self, html, base_url=QUrl()): self._widget.setHtml(html, base_url) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index d33b13795..9c6602224 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -245,7 +245,7 @@ class TabbedBrowser(QWidget): tab.new_tab_requested.connect(self.tabopen) if not self.is_private: web_history = objreg.get('web-history') - tab.add_history_item.connect(web_history.add_from_tab) + tab.history_item_triggered.connect(web_history.add_from_tab) def current_url(self): """Get the URL of the current tab. From e184ace8cb19effac80f196f54d4df445d9f503f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 08:55:15 +0100 Subject: [PATCH 118/258] Fix lint --- qutebrowser/browser/browsertab.py | 3 ++- qutebrowser/browser/webengine/webenginetab.py | 6 ++++-- qutebrowser/browser/webkit/webkittab.py | 10 +++++----- qutebrowser/config/configtypes.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 885d5d3f9..4bffbb796 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -685,7 +685,7 @@ class AbstractElements: def find_css(self, selector: str, callback: _MultiCallback, - error_callback: _ErrorCallback, *, + error_cb: _ErrorCallback, *, only_visible: bool = False) -> None: """Find all HTML elements matching a given selector async. @@ -694,6 +694,7 @@ class AbstractElements: Args: callback: The callback to be called when the search finished. + error_cb: The callback to be called when an error occurred. selector: The CSS selector to search for. only_visible: Only show elements which are visible on screen. """ diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index cf1649ed6..a74d866ea 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -24,8 +24,8 @@ import functools import re import html as html_utils -from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, - QUrl, QTimer, QObject) +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl, + QTimer, QObject) from PyQt5.QtGui import QIcon from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication @@ -520,6 +520,8 @@ class WebEngineScroller(browsertab.AbstractScroller): class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate): + """History-related methods which are not part of the extension API.""" + def serialize(self): if not qtutils.version_check('5.9', compiled=False): # WORKAROUND for diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index d0ff650b5..c10c2aeec 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -23,8 +23,7 @@ import re import functools import xml.etree.ElementTree -from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, - QSize) +from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize from PyQt5.QtGui import QIcon from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings @@ -511,6 +510,8 @@ class WebKitScroller(browsertab.AbstractScroller): class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate): + """History-related methods which are not part of the extension API.""" + def serialize(self): return qtutils.serialize(self._history) @@ -600,8 +601,7 @@ class WebKitElements(browsertab.AbstractElements): # Escape non-alphanumeric characters in the selector # https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id) - self.find_css('#' + elem_id, find_id_cb, - error_callback=lambda exc: None) + self.find_css('#' + elem_id, find_id_cb, error_cb=lambda exc: None) def find_focused(self, callback): frame = self._widget.page().currentFrame() @@ -727,7 +727,7 @@ class WebKitTab(browsertab.AbstractTab): url, emit_before_load_started=emit_before_load_started) self._widget.load(url) - def url(self, requested=False): + def url(self, *, requested=False): frame = self._widget.page().mainFrame() if requested: return frame.requestedUrl() diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index c0f3bec9f..d4ce33d21 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -923,7 +923,7 @@ class IgnoreCase(MappingType): ('always', "Search case-insensitively."), ('never', "Search case-sensitively."), ('smart', ("Search case-sensitively if there are capital " - "characters.")))), + "characters.")))) MAPPING = { 'always': usertypes.IgnoreCase.always, From 536a7ef1e65f0559137321467dddb7fe6bf15090 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 09:51:45 +0100 Subject: [PATCH 119/258] Move command registering to Command object --- qutebrowser/api/cmdutils.py | 8 ++------ qutebrowser/commands/command.py | 9 +++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index a90471a35..f95e984ca 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -117,14 +117,10 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name else: assert isinstance(self._name, str), self._name name = self._name - log.commands.vdebug( # type: ignore - "Registering command {} (from {}:{})" - .format(name, func.__module__, func.__qualname__)) - if name in objects.commands: - raise ValueError("{} is already registered!".format(name)) + cmd = command.Command(name=name, instance=self._instance, handler=func, **self._kwargs) - objects.commands[name] = cmd + cmd.register() return func diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 5c5ab1311..5422a9984 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -521,3 +521,12 @@ class Command: def takes_count(self): """Return true iff this command can take a count argument.""" return any(arg.count for arg in self._qute_args) + + def register(self): + """Register this command in objects.commands.""" + log.commands.vdebug( # type: ignore + "Registering command {} (from {}:{})".format( + self.name, self.handler.__module__, self.handler.__qualname__)) + if self.name in objects.commands: + raise ValueError("{} is already registered!".format(self.name)) + objects.commands[self.name] = self From 9c4ebb97d79eb62018a2b5a635f56123ad1cdb37 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 11:06:19 +0100 Subject: [PATCH 120/258] Fix refactoring issues --- qutebrowser/api/cmdutils.py | 3 +-- qutebrowser/commands/command.py | 2 +- tests/end2end/features/misc.feature | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index f95e984ca..10acf9b4d 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -22,8 +22,7 @@ import inspect import typing -from qutebrowser.misc import objects -from qutebrowser.utils import qtutils, log +from qutebrowser.utils import qtutils from qutebrowser.commands import command, cmdexc diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 5422a9984..811bc78eb 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -524,7 +524,7 @@ class Command: def register(self): """Register this command in objects.commands.""" - log.commands.vdebug( # type: ignore + log.commands.vdebug( "Registering command {} (from {}:{})".format( self.name, self.handler.__module__, self.handler.__qualname__)) if self.name in objects.commands: diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index a8e81b7b5..b9677a158 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -197,7 +197,7 @@ Feature: Various utility commands. # We can't use "When I open" because we don't want to wait for load # finished When I run :open http://localhost:(port)/redirect-later?delay=-1 - And I wait for "emitting: cur_load_status_changed('loading') (tab *)" in the log + And I wait for "emitting: cur_load_status_changed() (tab *)" in the log And I wait 1s And I run :stop And I open redirect-later-continue in a new tab From b3fa43a97cca0d6831647f0edae59387aa1f4564 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 11:17:49 +0100 Subject: [PATCH 121/258] Fix broken test_on_tab_changed --- qutebrowser/mainwindow/statusbar/url.py | 1 + tests/unit/mainwindow/statusbar/test_url.py | 26 ++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index 8ac258465..c6f436617 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -119,6 +119,7 @@ class UrlText(textbase.TextBase): Args: status: The usertypes.LoadStatus. """ + assert isinstance(status, usertypes.LoadStatus), status if status in [usertypes.LoadStatus.success, usertypes.LoadStatus.success_https, usertypes.LoadStatus.error, diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index 801a71f0a..7d06cb774 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -94,20 +94,28 @@ def test_on_load_status_changed(url_widget, status, expected): @pytest.mark.parametrize('load_status, qurl', [ - (url.UrlType.success, QUrl('http://abc123.com/this/awesome/url.html')), - (url.UrlType.success, QUrl('http://reddit.com/r/linux')), - (url.UrlType.success, QUrl('http://ä.com/')), - (url.UrlType.success_https, QUrl('www.google.com')), - (url.UrlType.success_https, QUrl('https://supersecret.gov/nsa/files.txt')), - (url.UrlType.warn, QUrl('www.shadysite.org/some/file/with/issues.htm')), - (url.UrlType.error, QUrl('invalid::/url')), - (url.UrlType.error, QUrl()), + (usertypes.LoadStatus.success, + QUrl('http://abc123.com/this/awesome/url.html')), + (usertypes.LoadStatus.success, + QUrl('http://reddit.com/r/linux')), + (usertypes.LoadStatus.success, + QUrl('http://ä.com/')), + (usertypes.LoadStatus.success_https, + QUrl('www.google.com')), + (usertypes.LoadStatus.success_https, + QUrl('https://supersecret.gov/nsa/files.txt')), + (usertypes.LoadStatus.warn, + QUrl('www.shadysite.org/some/file/with/issues.htm')), + (usertypes.LoadStatus.error, + QUrl('invalid::/url')), + (usertypes.LoadStatus.error, + QUrl()), ]) def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl): tab_widget = fake_web_tab(load_status=load_status, url=qurl) url_widget.on_tab_changed(tab_widget) - assert url_widget._urltype == load_status + assert url_widget._urltype.name == load_status.name if not qurl.isValid(): expected = '' else: From d549d4d5e2aff841150095b18681a75a10b36bbd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 11:19:03 +0100 Subject: [PATCH 122/258] Fix test_configcommands --- tests/unit/config/test_configcommands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 209d8db44..21c01ea5c 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -466,7 +466,8 @@ class TestSource: assert not config_stub.val.content.javascript.enabled ignore_case = config_stub.val.search.ignore_case - assert ignore_case == ('smart' if clear else 'always') + assert ignore_case == (usertypes.IgnoreCase.smart if clear + else usertypes.IgnoreCase.always) def test_errors(self, commands, config_tmpdir): pyfile = config_tmpdir / 'config.py' From 569bb0fa0989ae3a87d7725d2a19c9d746bf42d8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 12:45:01 +0100 Subject: [PATCH 123/258] Add cmdutils.Value instead of using count=True/win_id=True --- doc/contributing.asciidoc | 5 +- qutebrowser/api/cmdutils.py | 3 + qutebrowser/browser/commands.py | 76 ++++++++++----------- qutebrowser/browser/downloads.py | 10 +-- qutebrowser/commands/command.py | 42 ++++++------ qutebrowser/config/configcommands.py | 4 +- qutebrowser/keyinput/macros.py | 6 +- qutebrowser/mainwindow/statusbar/command.py | 2 +- qutebrowser/misc/sessions.py | 2 +- qutebrowser/misc/utilcmds.py | 20 +++--- qutebrowser/utils/usertypes.py | 8 +++ tests/unit/api/test_cmdutils.py | 40 ++++++++--- tests/unit/completion/test_completer.py | 4 +- 13 files changed, 128 insertions(+), 94 deletions(-) diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 5f0e9bf9c..399633257 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -480,8 +480,9 @@ For `typing.Union` types, the given `choices` are only checked if other types The following arguments are supported for `@cmdutils.argument`: - `flag`: Customize the short flag (`-x`) the argument will get. -- `win_id=True`: Mark the argument as special window ID argument. -- `count=True`: Mark the argument as special count argument. +- `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. - `completion`: A completion function (see `qutebrowser.completions.models.*`) to use when completing arguments for the given command. - `choices`: The allowed string choices for the argument. diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 10acf9b4d..796c74eba 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -21,9 +21,12 @@ import inspect import typing +import enum from qutebrowser.utils import qtutils from qutebrowser.commands import command, cmdexc +# pylint: disable=unused-import +from qutebrowser.utils.usertypes import CommandValue as Value class CommandError(cmdexc.Error): diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 63c2da3b3..f4703850a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -230,7 +230,7 @@ class CommandDispatcher: tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_close(self, prev=False, next_=False, opposite=False, force=False, count=None): """Close the current/[count]th tab. @@ -253,7 +253,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_pin(self, count=None): """Pin/Unpin the current/[count]th tab. @@ -274,7 +274,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') @cmdutils.argument('url', completion=urlmodel.url) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def openurl(self, url=None, related=False, bg=False, tab=False, window=False, count=None, secure=False, private=False): @@ -372,7 +372,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='reload', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def reloadpage(self, force=False, count=None): """Reload the current/[count]th tab. @@ -385,7 +385,7 @@ class CommandDispatcher: tab.reload(force=force) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def stop(self, count=None): """Stop loading in the current/[count]th tab. @@ -423,7 +423,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', name='print', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('pdf', flag='f', metavar='file') def printpage(self, preview=False, count=None, *, pdf=None): """Print the current/[count]th tab. @@ -514,7 +514,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('win_id', completion=miscmodels.window) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_give(self, win_id: int = None, keep: bool = False, count: int = None) -> None: """Give the current tab to a new or existing window if win_id given. @@ -575,7 +575,7 @@ class CommandDispatcher: raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def back(self, tab=False, bg=False, window=False, count=1): """Go back in the history of the current tab. @@ -588,7 +588,7 @@ class CommandDispatcher: self._back_forward(tab, bg, window, count, forward=False) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def forward(self, tab=False, bg=False, window=False, count=1): """Go forward in the history of the current tab. @@ -603,7 +603,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment', 'decrement']) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def navigate(self, where: str, tab: bool = False, bg: bool = False, window: bool = False, count: int = 1) -> None: """Open typical prev/next links or navigate using the URL path. @@ -668,7 +668,7 @@ class CommandDispatcher: raise cmdutils.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def scroll_px(self, dx: int, dy: int, count: int = 1) -> None: """Scroll the current tab by 'count * dx/dy' pixels. @@ -684,7 +684,7 @@ class CommandDispatcher: self._current_widget().scroller.delta(dx, dy) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def scroll(self, direction: str, count: int = 1) -> None: """Scroll the current tab in the given direction. @@ -721,7 +721,7 @@ class CommandDispatcher: func(count=count) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('horizontal', flag='x') def scroll_to_perc(self, perc: float = None, horizontal: bool = False, count: int = None) -> None: @@ -762,7 +762,7 @@ class CommandDispatcher: self._current_widget().scroller.to_anchor(name) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('top_navigate', metavar='ACTION', choices=('prev', 'decrement')) @cmdutils.argument('bottom_navigate', metavar='ACTION', @@ -891,7 +891,7 @@ class CommandDispatcher: maybe=True) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def zoom_in(self, count=1, quiet=False): """Increase the zoom level for the current tab. @@ -908,7 +908,7 @@ class CommandDispatcher: message.info("Zoom level: {}%".format(int(perc)), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def zoom_out(self, count=1, quiet=False): """Decrease the zoom level for the current tab. @@ -925,7 +925,7 @@ class CommandDispatcher: message.info("Zoom level: {}%".format(int(perc)), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def zoom(self, zoom=None, count=None, quiet=False): """Set the zoom level for the current tab. @@ -1005,7 +1005,7 @@ class CommandDispatcher: raise cmdutils.CommandError("Nothing to undo!") @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_prev(self, count=1): """Switch to the previous tab, or switch [count] tabs back. @@ -1025,7 +1025,7 @@ class CommandDispatcher: log.webview.debug("First tab") @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_next(self, count=1): """Switch to the next tab, or switch [count] tabs forward. @@ -1093,7 +1093,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) @cmdutils.argument('index', completion=miscmodels.buffer) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def buffer(self, index=None, count=None): """Select tab by index or url/title best match. @@ -1123,7 +1123,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['last']) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_focus(self, index: typing.Union[str, int] = None, count: int = None, no_last: bool = False) -> None: """Select the tab given as argument/[count]. @@ -1165,7 +1165,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['+', '-']) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_move(self, index: typing.Union[str, int] = None, count: int = None) -> None: """Move the current tab according to the argument and [count]. @@ -1212,7 +1212,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_replace_variables=True) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def spawn(self, cmdline, userscript=False, verbose=False, output=False, detach=False, count=None): """Spawn a command in a shell. @@ -1830,7 +1830,7 @@ class CommandDispatcher: tab.search.search(text, **options) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def search_next(self, count=1): """Continue the search to the ([count]th) next term. @@ -1864,7 +1864,7 @@ class CommandDispatcher: tab.search.next_result(result_cb=cb) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def search_prev(self, count=1): """Continue the search to the ([count]th) previous term. @@ -1899,7 +1899,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_line(self, count=1): """Move the cursor or selection to the next line. @@ -1910,7 +1910,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_line(self, count=1): """Move the cursor or selection to the prev line. @@ -1921,7 +1921,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_char(self, count=1): """Move the cursor or selection to the next char. @@ -1932,7 +1932,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_char(self, count=1): """Move the cursor or selection to the previous char. @@ -1943,7 +1943,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_word(self, count=1): """Move the cursor or selection to the end of the word. @@ -1954,7 +1954,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_word(self, count=1): """Move the cursor or selection to the next word. @@ -1965,7 +1965,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_word(self, count=1): """Move the cursor or selection to the previous word. @@ -1988,7 +1988,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_start_of_next_block(self, count=1): """Move the cursor or selection to the start of next block. @@ -1999,7 +1999,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_start_of_prev_block(self, count=1): """Move the cursor or selection to the start of previous block. @@ -2010,7 +2010,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_next_block(self, count=1): """Move the cursor or selection to the end of next block. @@ -2021,7 +2021,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_prev_block(self, count=1): """Move the cursor or selection to the end of previous block. @@ -2056,7 +2056,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', debug=True) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def debug_webaction(self, action, count=1): """Execute a webaction. @@ -2256,7 +2256,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-mute') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def tab_mute(self, count=None): """Mute/Unmute the current/[count]th tab. diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 6d9fa6c4e..b18e426d7 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1013,7 +1013,7 @@ class DownloadModel(QAbstractListModel): raise cmdutils.CommandError("There's no download {}!".format(count)) @cmdutils.register(instance='download-model', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def download_cancel(self, all_=False, count=0): """Cancel the last/[count]th download. @@ -1039,7 +1039,7 @@ class DownloadModel(QAbstractListModel): download.cancel() @cmdutils.register(instance='download-model', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def download_delete(self, count=0): """Delete the last/[count]th download from disk. @@ -1060,7 +1060,7 @@ class DownloadModel(QAbstractListModel): log.downloads.debug("deleted download {}".format(download)) @cmdutils.register(instance='download-model', scope='window', maxsplit=0) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def download_open(self, cmdline: str = None, count: int = 0) -> None: """Open the last/[count]th download. @@ -1086,7 +1086,7 @@ class DownloadModel(QAbstractListModel): download.open_file(cmdline) @cmdutils.register(instance='download-model', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def download_retry(self, count=0): """Retry the first failed/[count]th download. @@ -1121,7 +1121,7 @@ class DownloadModel(QAbstractListModel): download.remove() @cmdutils.register(instance='download-model', scope='window') - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def download_remove(self, all_=False, count=0): """Remove the last/[count]th download from the list. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 811bc78eb..a318897d5 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -37,18 +37,13 @@ class ArgInfo: """Information about an argument.""" - win_id = attr.ib(False) - count = attr.ib(False) + value = attr.ib(None) hide = attr.ib(False) metavar = attr.ib(None) flag = attr.ib(None) completion = attr.ib(None) choices = attr.ib(None) - def __attrs_post_init__(self): - if self.win_id and self.count: - raise TypeError("Argument marked as both count/win_id!") - class Command: @@ -116,7 +111,6 @@ class Command: self.parser.add_argument('-h', '--help', action=argparser.HelpAction, default=argparser.SUPPRESS, nargs=0, help=argparser.SUPPRESS) - self._check_func() self.opt_args = collections.OrderedDict() self.namespace = None self._count = None @@ -130,6 +124,7 @@ class Command: self._qute_args = getattr(self.handler, 'qute_args', {}) self.handler.qute_args = None + self._check_func() self._inspect_func() def _check_prerequisites(self, win_id): @@ -154,9 +149,14 @@ class Command: def _check_func(self): """Make sure the function parameters don't violate any rules.""" signature = inspect.signature(self.handler) - if 'self' in signature.parameters and self._instance is None: - raise TypeError("{} is a class method, but instance was not " - "given!".format(self.name[0])) + if 'self' in signature.parameters: + if self._instance is None: + raise TypeError("{} is a class method, but instance was not " + "given!".format(self.name[0])) + arg_info = self.get_arg_info(signature.parameters['self']) + if arg_info.value is not None: + raise TypeError("{}: Can't fill 'self' with value!" + .format(self.name)) elif 'self' not in signature.parameters and self._instance is not None: raise TypeError("{} is not a class method, but instance was " "given!".format(self.name[0])) @@ -186,13 +186,18 @@ class Command: True if the parameter is special, False otherwise. """ arg_info = self.get_arg_info(param) - if arg_info.count: + if arg_info.value is None: + return False + elif arg_info.value == usertypes.CommandValue.count: if param.default is inspect.Parameter.empty: raise TypeError("{}: handler has count parameter " "without default!".format(self.name)) return True - elif arg_info.win_id: + elif arg_info.value == usertypes.CommandValue.win_id: return True + else: + raise TypeError("{}: Invalid value={!r} for argument '{}'!" + .format(self.name, arg_info.value, param.name)) return False def _inspect_func(self): @@ -325,9 +330,8 @@ class Command: return param.annotation elif param.default not in [None, inspect.Parameter.empty]: return type(param.default) - elif arginfo.count or arginfo.win_id or param.kind in [ - inspect.Parameter.VAR_POSITIONAL, - inspect.Parameter.VAR_KEYWORD]: + elif arginfo.value or param.kind in [inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD]: return None else: return str @@ -447,15 +451,13 @@ class Command: # Special case for 'self'. self._get_self_arg(win_id, param, args) continue - elif arg_info.count: - # Special case for count parameter. + elif arg_info.value == usertypes.CommandValue.count: self._get_count_arg(param, args, kwargs) continue - # elif arg_info.win_id: - elif arg_info.win_id: - # Special case for win_id parameter. + elif arg_info.value == usertypes.CommandValue.win_id: self._get_win_id_arg(win_id, param, args, kwargs) continue + value = self._get_param_value(param) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(value) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 74a381507..574bc06af 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -79,7 +79,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('value', completion=configmodel.value) - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('pattern', flag='u') def set(self, win_id, option=None, value=None, temp=False, print_=False, *, pattern=None): @@ -127,7 +127,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def bind(self, win_id, key=None, command=None, *, mode='normal', default=False): """Bind a key to a command. diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index bd17f5664..5bf1ab18b 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -45,7 +45,7 @@ class MacroRecorder: self._last_register = None @cmdutils.register(instance='macro-recorder', name='record-macro') - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def record_macro_command(self, win_id, register=None): """Start or stop recording a macro. @@ -70,8 +70,8 @@ class MacroRecorder: self._recording_macro = register @cmdutils.register(instance='macro-recorder', name='run-macro') - @cmdutils.argument('win_id', win_id=True) - @cmdutils.argument('count', count=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) + @cmdutils.argument('count', value=cmdutils.Value.count) def run_macro_command(self, win_id, count=1, register=None): """Run a recorded macro. diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index de42cda4e..1661d2362 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -115,7 +115,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): @cmdutils.register(instance='status-command', name='set-cmd-text', scope='window', maxsplit=0) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def set_cmd_text_command(self, text, count=None, space=False, append=False, run_on_count=False): """Preset the statusbar to some text. diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 357d5be64..2a557ef50 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -508,7 +508,7 @@ class SessionManager(QObject): @cmdutils.register(instance='session-manager') @cmdutils.argument('name', completion=miscmodels.session) - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('with_private', flag='p') def session_save(self, name: typing.Union[str, Sentinel] = default, current: bool = False, diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 761498e5f..287704551 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -44,7 +44,7 @@ from qutebrowser.qt import sip @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) -@cmdutils.argument('win_id', win_id=True) +@cmdutils.argument('win_id', value=cmdutils.Value.win_id) def later(ms: int, command: str, win_id: int) -> None: """Execute a command after some time. @@ -74,8 +74,8 @@ def later(ms: int, command: str, win_id: int) -> None: @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) -@cmdutils.argument('win_id', win_id=True) -@cmdutils.argument('count', count=True) +@cmdutils.argument('win_id', value=cmdutils.Value.win_id) +@cmdutils.argument('count', value=cmdutils.Value.count) def repeat(times: int, command: str, win_id: int, count: int = None) -> None: """Repeat a given command. @@ -95,8 +95,8 @@ def repeat(times: int, command: str, win_id: int, count: int = None) -> None: @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) -@cmdutils.argument('win_id', win_id=True) -@cmdutils.argument('count', count=True) +@cmdutils.argument('win_id', value=cmdutils.Value.win_id) +@cmdutils.argument('count', value=cmdutils.Value.count) def run_with_count(count_arg: int, command: str, win_id: int, count: int = 1) -> None: """Run a command with the given count. @@ -122,7 +122,7 @@ def message_error(text): @cmdutils.register() -@cmdutils.argument('count', count=True) +@cmdutils.argument('count', value=cmdutils.Value.count) def message_info(text, count=1): """Show an info message in the statusbar. @@ -288,8 +288,8 @@ def debug_set_fake_clipboard(s=None): @cmdutils.register() -@cmdutils.argument('win_id', win_id=True) -@cmdutils.argument('count', count=True) +@cmdutils.argument('win_id', value=cmdutils.Value.win_id) +@cmdutils.argument('count', value=cmdutils.Value.count) def repeat_command(win_id, count=None): """Repeat the last executed command. @@ -358,7 +358,7 @@ def debug_log_filter(filters: str) -> None: @cmdutils.register() -@cmdutils.argument('current_win_id', win_id=True) +@cmdutils.argument('current_win_id', value=cmdutils.Value.win_id) def window_only(current_win_id): """Close all windows except for the current one.""" for win_id, window in objreg.window_registry.items(): @@ -377,7 +377,7 @@ def nop(): @cmdutils.register() -@cmdutils.argument('win_id', win_id=True) +@cmdutils.argument('win_id', value=cmdutils.Value.win_id) def version(win_id, paste=False): """Show version information. diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 5bb8d3aa9..d8f46ded8 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -256,6 +256,14 @@ MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info']) IgnoreCase = enum.Enum('IgnoreCase', ['smart', 'never', 'always']) +class CommandValue(enum.Enum): + + """Special values which are injected when running a command handler.""" + + count = 1 + win_id = 2 + + class Question(QObject): """A question asked to the user, e.g. via the status bar. diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index f2318ab46..5e1389c80 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -190,23 +190,39 @@ class TestRegister: def test_win_id(self): @cmdutils.register() - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def fun(win_id): """Blah.""" assert objects.commands['fun']._get_call_args(42) == ([42], {}) def test_count(self): @cmdutils.register() - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def fun(count=0): """Blah.""" assert objects.commands['fun']._get_call_args(42) == ([0], {}) + def test_fill_self(self): + with pytest.raises(TypeError, match="fun: Can't fill 'self' with " + "value!"): + @cmdutils.register(instance='foobar') + @cmdutils.argument('self', value=cmdutils.Value.count) + def fun(self): + """Blah.""" + + def test_fill_invalid(self): + with pytest.raises(TypeError, match="fun: Invalid value='foo' for " + "argument 'arg'!"): + @cmdutils.register() + @cmdutils.argument('arg', value='foo') + def fun(arg): + """Blah.""" + def test_count_without_default(self): with pytest.raises(TypeError, match="fun: handler has count parameter " "without default!"): @cmdutils.register() - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def fun(count): """Blah.""" @@ -344,6 +360,17 @@ class TestArgument: } assert fun.qute_args == expected + def test_arginfo_boolean(self): + @cmdutils.argument('special1', value=cmdutils.Value.count) + @cmdutils.argument('special2', value=cmdutils.Value.win_id) + @cmdutils.argument('normal') + def fun(special1, special2, normal): + """Blah.""" + + assert fun.qute_args['special1'].value + assert fun.qute_args['special2'].value + assert not fun.qute_args['normal'].value + def test_wrong_order(self): """When @cmdutils.argument is used above (after) @register, fail.""" with pytest.raises(ValueError, match=r"@cmdutils.argument got called " @@ -353,13 +380,6 @@ class TestArgument: def fun(bar): """Blah.""" - def test_count_and_win_id_same_arg(self): - with pytest.raises(TypeError, - match="Argument marked as both count/win_id!"): - @cmdutils.argument('arg', count=True, win_id=True) - def fun(arg=0): - """Blah.""" - def test_no_docstring(self, caplog): with caplog.at_level(logging.WARNING): @cmdutils.register() diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 16ebb95dc..224268c90 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -112,12 +112,12 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch): """docstring.""" @cmdutils.argument('url', completion=miscmodels_patch.url) - @cmdutils.argument('count', count=True) + @cmdutils.argument('count', value=cmdutils.Value.count) def openurl(url=None, related=False, bg=False, tab=False, window=False, count=None): """docstring.""" - @cmdutils.argument('win_id', win_id=True) + @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('command', completion=miscmodels_patch.command) def bind(key, win_id, command=None, *, mode='normal'): """docstring.""" From 8725ac6e667fe106edf446a5e61e282024ba32ac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 12:49:57 +0100 Subject: [PATCH 124/258] Fix type of stack argument for utils.message --- qutebrowser/utils/message.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 9dc8d7411..035cc1c71 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -34,15 +34,10 @@ def _log_stack(typ, stack): Args: typ: The type of the message (str) - stack: The stack as an iterable of strings or a single string + stack: The stacktrace as a string. """ - try: - # traceback.format_exc() produces a list of strings, while - # traceback.format_stack() produces a single string... - stack = stack.splitlines() - except AttributeError: - pass - stack_text = '\n'.join(line.rstrip() for line in stack) + lines = stack.splitlines() + stack_text = '\n'.join(line.rstrip() for line in lines) log.message.debug("Stack for {} message:\n{}".format(typ, stack_text)) @@ -55,7 +50,7 @@ def error(message, *, stack=None, replace=False): replace: Replace existing messages with replace=True """ if stack is None: - stack = traceback.format_stack() + stack = ''.join(traceback.format_stack()) typ = 'error' else: typ = 'error (from exception)' @@ -71,7 +66,7 @@ def warning(message, *, replace=False): message: The message to show replace: Replace existing messages with replace=True """ - _log_stack('warning', traceback.format_stack()) + _log_stack('warning', ''.join(traceback.format_stack())) log.message.warning(message) global_bridge.show(usertypes.MessageLevel.warning, message, replace) From fe6f6273059f67a148e8fba405e221c1a3616821 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 12:51:26 +0100 Subject: [PATCH 125/258] Add partial type annotations for utils.message --- qutebrowser/utils/message.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 035cc1c71..b496273f8 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -29,19 +29,19 @@ from PyQt5.QtCore import pyqtSignal, QObject from qutebrowser.utils import usertypes, log, utils -def _log_stack(typ, stack): +def _log_stack(typ: str, stack: str) -> None: """Log the given message stacktrace. Args: - typ: The type of the message (str) - stack: The stacktrace as a string. + typ: The type of the message. + stack: An optional stacktrace. """ lines = stack.splitlines() stack_text = '\n'.join(line.rstrip() for line in lines) log.message.debug("Stack for {} message:\n{}".format(typ, stack_text)) -def error(message, *, stack=None, replace=False): +def error(message: str, *, stack: str = None, replace: bool = False) -> None: """Convenience function to display an error message in the statusbar. Args: @@ -59,7 +59,7 @@ def error(message, *, stack=None, replace=False): global_bridge.show(usertypes.MessageLevel.error, message, replace) -def warning(message, *, replace=False): +def warning(message: str, *, replace: bool = False) -> None: """Convenience function to display a warning message in the statusbar. Args: @@ -71,7 +71,7 @@ def warning(message, *, replace=False): global_bridge.show(usertypes.MessageLevel.warning, message, replace) -def info(message, *, replace=False): +def info(message: str, *, replace: bool = False) -> None: """Convenience function to display an info message in the statusbar. Args: From 566304ab4e0788c2fea03cb4300f40651a0f9429 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 12:51:54 +0100 Subject: [PATCH 126/258] Fix lint --- qutebrowser/api/cmdutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 796c74eba..3a1814856 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -21,7 +21,6 @@ import inspect import typing -import enum from qutebrowser.utils import qtutils from qutebrowser.commands import command, cmdexc From 7a90340299457f53bab037bfdcb3d698facb6d55 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 12:59:21 +0100 Subject: [PATCH 127/258] Unify _get_count_arg/_get_win_id_arg --- qutebrowser/commands/command.py | 41 ++++++++++++--------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index a318897d5..cd7159878 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -358,39 +358,26 @@ class Command: tab=tab_id) args.append(obj) - def _get_count_arg(self, param, args, kwargs): - """Add the count argument to a function call. + def _add_special_arg(self, *, value, param, args, kwargs, optional=False): + """Add a special argument value to a function call. Arguments: - param: The count parameter. + value: The value to add. + param: The parameter being filled. args: The positional argument list. Gets modified directly. kwargs: The keyword argument dict. Gets modified directly. + optional: Whether the value can be optional. """ + if not optional: + assert value is not None if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - if self._count is not None: - args.append(self._count) + if value is not None: + args.append(value) else: args.append(param.default) elif param.kind == inspect.Parameter.KEYWORD_ONLY: - if self._count is not None: - kwargs[param.name] = self._count - else: - raise TypeError("{}: invalid parameter type {} for argument " - "{!r}!".format(self.name, param.kind, param.name)) - - def _get_win_id_arg(self, win_id, param, args, kwargs): - """Add the win_id argument to a function call. - - Arguments: - win_id: The window ID to add. - param: The count parameter. - args: The positional argument list. Gets modified directly. - kwargs: The keyword argument dict. Gets modified directly. - """ - if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - args.append(win_id) - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - kwargs[param.name] = win_id + if value is not None: + kwargs[param.name] = value else: raise TypeError("{}: invalid parameter type {} for argument " "{!r}!".format(self.name, param.kind, param.name)) @@ -452,10 +439,12 @@ class Command: self._get_self_arg(win_id, param, args) continue elif arg_info.value == usertypes.CommandValue.count: - self._get_count_arg(param, args, kwargs) + self._add_special_arg(value=self._count, param=param, + args=args, kwargs=kwargs, optional=True) continue elif arg_info.value == usertypes.CommandValue.win_id: - self._get_win_id_arg(win_id, param, args, kwargs) + self._add_special_arg(value=win_id, param=param, + args=args, kwargs=kwargs) continue value = self._get_param_value(param) From 4e56d0e8b35db0cca724c6b112f2e36a54a15601 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 13:06:53 +0100 Subject: [PATCH 128/258] Refactor Command._get_self_arg --- qutebrowser/commands/command.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index cd7159878..74cf659f7 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -336,27 +336,18 @@ class Command: else: return str - def _get_self_arg(self, win_id, param, args): - """Get the self argument for a function call. - - Arguments: - win_id: The window id this command should be executed in. - param: The count parameter. - args: The positional argument list. Gets modified directly. - """ - assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD - if self._scope == 'global': + def _get_objreg(self, *, win_id, name, scope): + """Get an object from the objreg.""" + if scope == 'global': tab_id = None win_id = None - elif self._scope == 'tab': + elif scope == 'tab': tab_id = 'current' - elif self._scope == 'window': + elif scope == 'window': tab_id = None else: - raise ValueError("Invalid scope {}!".format(self._scope)) - obj = objreg.get(self._instance, scope=self._scope, window=win_id, - tab=tab_id) - args.append(obj) + raise ValueError("Invalid scope {}!".format(scope)) + return objreg.get(name, scope=scope, window=win_id, tab=tab_id) def _add_special_arg(self, *, value, param, args, kwargs, optional=False): """Add a special argument value to a function call. @@ -435,8 +426,12 @@ class Command: for i, param in enumerate(signature.parameters.values()): arg_info = self.get_arg_info(param) if i == 0 and self._instance is not None: - # Special case for 'self'. - self._get_self_arg(win_id, param, args) + assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + self_value = self._get_objreg(win_id=win_id, + name=self._instance, + scope=self._scope) + self._add_special_arg(value=self_value, param=param, + args=args, kwargs=kwargs) continue elif arg_info.value == usertypes.CommandValue.count: self._add_special_arg(value=self._count, param=param, From 4ce8a6eaf6791abd2bf7b1f0a707dd8cb30ab279 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 13:07:44 +0100 Subject: [PATCH 129/258] Mark unreachable --- qutebrowser/commands/command.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 74cf659f7..51fe6cceb 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -27,7 +27,7 @@ import typing import attr from qutebrowser.commands import cmdexc, argparser -from qutebrowser.utils import log, message, docutils, objreg, usertypes +from qutebrowser.utils import log, message, docutils, objreg, usertypes, utils from qutebrowser.utils import debug as debug_utils from qutebrowser.misc import objects @@ -441,6 +441,10 @@ class Command: self._add_special_arg(value=win_id, param=param, args=args, kwargs=kwargs) continue + elif arg_info.value is None: + pass + else: + raise utils.Unreachable(arg_info) value = self._get_param_value(param) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: From c0d4fe0ab541c334e742816a1c0689d3c839812a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 13:14:58 +0100 Subject: [PATCH 130/258] Make it possible to inject tab via cmdutils.Value --- doc/contributing.asciidoc | 1 + qutebrowser/commands/command.py | 7 ++++++- qutebrowser/utils/usertypes.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index 399633257..b11014ca1 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -483,6 +483,7 @@ The following arguments are supported for `@cmdutils.argument`: - `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.tab`: The tab object which is currently focused. - `completion`: A completion function (see `qutebrowser.completions.models.*`) to use when completing arguments for the given command. - `choices`: The allowed string choices for the argument. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 51fe6cceb..631ecc354 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -193,7 +193,7 @@ class Command: raise TypeError("{}: handler has count parameter " "without default!".format(self.name)) return True - elif arg_info.value == usertypes.CommandValue.win_id: + elif isinstance(arg_info.value, usertypes.CommandValue): return True else: raise TypeError("{}: Invalid value={!r} for argument '{}'!" @@ -441,6 +441,11 @@ class Command: self._add_special_arg(value=win_id, param=param, args=args, kwargs=kwargs) continue + elif arg_info.value == usertypes.CommandValue.tab: + tab = self._get_objreg(win_id=win_id, name='tab', scope='tab') + self._add_special_arg(value=tab, param=param, + args=args, kwargs=kwargs) + continue elif arg_info.value is None: pass else: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index d8f46ded8..c6fddb039 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -262,6 +262,7 @@ class CommandValue(enum.Enum): count = 1 win_id = 2 + tab = 3 class Question(QObject): From ce3b1f252d2e96692f39db35152926106851e593 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 13:58:03 +0100 Subject: [PATCH 131/258] Add api.message and api.tab --- qutebrowser/api/message.py | 23 +++++++++++++++++++++++ qutebrowser/api/tab.py | 23 +++++++++++++++++++++++ qutebrowser/app.py | 1 + 3 files changed, 47 insertions(+) create mode 100644 qutebrowser/api/message.py create mode 100644 qutebrowser/api/tab.py diff --git a/qutebrowser/api/message.py b/qutebrowser/api/message.py new file mode 100644 index 000000000..fdb06354f --- /dev/null +++ b/qutebrowser/api/message.py @@ -0,0 +1,23 @@ +# 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 . + +"""Utilities to display messages above the status bar.""" + +# pylint: disable=unused-import +from qutebrowser.utils.message import error, warning, info diff --git a/qutebrowser/api/tab.py b/qutebrowser/api/tab.py new file mode 100644 index 000000000..444e49558 --- /dev/null +++ b/qutebrowser/api/tab.py @@ -0,0 +1,23 @@ +# 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 . + +"""A single tab.""" + +# pylint: disable=unused-import +from qutebrowser.browser.browsertab import AbstractTab as Tab diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 27848c4c1..e950ff2ec 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -77,6 +77,7 @@ 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 from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds # pylint: enable=unused-import From e6c6e0dd5943d6fc40ae0ed59db94bd7ad9de216 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 13:58:36 +0100 Subject: [PATCH 132/258] Move scroll commands to components/ --- qutebrowser/browser/commands.py | 94 ----------------- qutebrowser/components/__init__.py | 20 ++++ qutebrowser/components/scrollcommands.py | 123 +++++++++++++++++++++++ 3 files changed, 143 insertions(+), 94 deletions(-) create mode 100644 qutebrowser/components/__init__.py create mode 100644 qutebrowser/components/scrollcommands.py diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f4703850a..07f6156e6 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -667,100 +667,6 @@ class CommandDispatcher: except navigate.Error as e: raise cmdutils.CommandError(e) - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def scroll_px(self, dx: int, dy: int, count: int = 1) -> None: - """Scroll the current tab by 'count * dx/dy' pixels. - - Args: - dx: How much to scroll in x-direction. - dy: How much to scroll in y-direction. - count: multiplier - """ - dx *= count - dy *= count - cmdutils.check_overflow(dx, 'int') - cmdutils.check_overflow(dy, 'int') - self._current_widget().scroller.delta(dx, dy) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def scroll(self, direction: str, count: int = 1) -> None: - """Scroll the current tab in the given direction. - - Note you can use `:run-with-count` to have a keybinding with a bigger - scroll increment. - - Args: - direction: In which direction to scroll - (up/down/left/right/top/bottom). - count: multiplier - """ - tab = self._current_widget() - funcs = { - 'up': tab.scroller.up, - 'down': tab.scroller.down, - 'left': tab.scroller.left, - 'right': tab.scroller.right, - 'top': tab.scroller.top, - 'bottom': tab.scroller.bottom, - 'page-up': tab.scroller.page_up, - 'page-down': tab.scroller.page_down, - } - try: - func = funcs[direction] - except KeyError: - expected_values = ', '.join(sorted(funcs)) - raise cmdutils.CommandError("Invalid value {!r} for direction - " - "expected one of: {}".format( - direction, expected_values)) - - if direction in ['top', 'bottom']: - func() - else: - func(count=count) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - @cmdutils.argument('horizontal', flag='x') - def scroll_to_perc(self, perc: float = None, horizontal: bool = False, - count: int = None) -> None: - """Scroll to a specific percentage of the page. - - The percentage can be given either as argument or as count. - If no percentage is given, the page is scrolled to the end. - - Args: - perc: Percentage to scroll. - horizontal: Scroll horizontally instead of vertically. - count: Percentage to scroll. - """ - # save the pre-jump position in the special ' mark - self.set_mark("'") - - if perc is None and count is None: - perc = 100 - elif count is not None: - perc = count - - if horizontal: - x = perc - y = None - else: - x = None - y = perc - - self._current_widget().scroller.to_perc(x, y) - - @cmdutils.register(instance='command-dispatcher', scope='window') - def scroll_to_anchor(self, name): - """Scroll to the given anchor in the document. - - Args: - name: The anchor to scroll to. - """ - self._current_widget().scroller.to_anchor(name) - @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('top_navigate', metavar='ACTION', diff --git a/qutebrowser/components/__init__.py b/qutebrowser/components/__init__.py new file mode 100644 index 000000000..b42c87fb6 --- /dev/null +++ b/qutebrowser/components/__init__.py @@ -0,0 +1,20 @@ +# 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 . + +"""qutebrowser "extensions" which only use the qutebrowser.API API.""" diff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py new file mode 100644 index 000000000..13deb9b6b --- /dev/null +++ b/qutebrowser/components/scrollcommands.py @@ -0,0 +1,123 @@ +# 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 . + +"""Scrolling-related commands.""" + +from qutebrowser.api import cmdutils, tab + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def scroll_px(tab: tab.Tab, dx: int, dy: int, count: int = 1) -> None: + """Scroll the current tab by 'count * dx/dy' pixels. + + Args: + dx: How much to scroll in x-direction. + dy: How much to scroll in y-direction. + count: multiplier + """ + dx *= count + dy *= count + cmdutils.check_overflow(dx, 'int') + cmdutils.check_overflow(dy, 'int') + tab.scroller.delta(dx, dy) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def scroll(tab: tab.Tab, direction: str, count: int = 1) -> None: + """Scroll the current tab in the given direction. + + Note you can use `:run-with-count` to have a keybinding with a bigger + scroll increment. + + Args: + direction: In which direction to scroll + (up/down/left/right/top/bottom). + count: multiplier + """ + funcs = { + 'up': tab.scroller.up, + 'down': tab.scroller.down, + 'left': tab.scroller.left, + 'right': tab.scroller.right, + 'top': tab.scroller.top, + 'bottom': tab.scroller.bottom, + 'page-up': tab.scroller.page_up, + 'page-down': tab.scroller.page_down, + } + try: + func = funcs[direction] + except KeyError: + expected_values = ', '.join(sorted(funcs)) + raise cmdutils.CommandError("Invalid value {!r} for direction - " + "expected one of: {}".format( + direction, expected_values)) + + if direction in ['top', 'bottom']: + func() + else: + func(count=count) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +@cmdutils.argument('horizontal', flag='x') +def scroll_to_perc(tab: tab.Tab, count: int = None, + perc: float = None, horizontal: bool = False) -> None: + """Scroll to a specific percentage of the page. + + The percentage can be given either as argument or as count. + If no percentage is given, the page is scrolled to the end. + + Args: + perc: Percentage to scroll. + horizontal: Scroll horizontally instead of vertically. + count: Percentage to scroll. + """ + # save the pre-jump position in the special ' mark + self.set_mark("'") + + if perc is None and count is None: + perc = 100 + elif count is not None: + perc = count + + if horizontal: + x = perc + y = None + else: + x = None + y = perc + + tab.scroller.to_perc(x, y) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +def scroll_to_anchor(tab: tab.Tab, name): + """Scroll to the given anchor in the document. + + Args: + name: The anchor to scroll to. + """ + tab.scroller.to_anchor(name) From 88205a8d322f76d6f30f4f930bbc3c6cc1b72cee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:17:40 +0100 Subject: [PATCH 133/258] Add AbstractScroller.before_jump_requested signal This allows us to save the ' mark without needing access to the TabbedBrowser object. This also changes the places the ' mark is saved slightly: - :navigate doesn't save it anymore as there is no reason to do so (loading the new page will render the mark useless anyways). - When clearing a search, the ' mark isn't saved. - :scroll-anchor now saves the ' mark. --- qutebrowser/browser/browsertab.py | 10 +++++++++- qutebrowser/browser/commands.py | 9 +++------ qutebrowser/browser/hints.py | 4 +--- qutebrowser/components/scrollcommands.py | 5 ++--- qutebrowser/mainwindow/tabbedbrowser.py | 4 ++-- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 4bffbb796..3bd4c55c3 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -522,9 +522,17 @@ class AbstractCaret(QObject): class AbstractScroller(QObject): - """Attribute of AbstractTab to manage scroll position.""" + """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. + """ perc_changed = pyqtSignal(int, int) + before_jump_requested = pyqtSignal() def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 07f6156e6..791e45a1a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -634,9 +634,6 @@ class CommandDispatcher: count: For `increment` and `decrement`, the number to change the URL by. For `up`, the number of levels to go up in the URL. """ - # save the pre-jump position in the special ' mark - self.set_mark("'") - cmdutils.check_exclusive((tab, bg, window), 'tbw') widget = self._current_widget() url = self._current_url() @@ -1712,7 +1709,6 @@ class CommandDispatcher: text: The text to search for. reverse: Reverse search direction. """ - self.set_mark("'") tab = self._current_widget() if not text: @@ -1733,6 +1729,7 @@ class CommandDispatcher: options=options, text=text, prev=False) options['result_cb'] = cb + tab.scroller.before_jump_requested.emit() tab.search.search(text, **options) @cmdutils.register(instance='command-dispatcher', scope='window') @@ -1750,7 +1747,7 @@ class CommandDispatcher: if window_text is None: raise cmdutils.CommandError("No search done yet.") - self.set_mark("'") + tab.scroller.before_jump_requested.emit() if window_text is not None and window_text != tab.search.text: tab.search.clear() @@ -1784,7 +1781,7 @@ class CommandDispatcher: if window_text is None: raise cmdutils.CommandError("No search done yet.") - self.set_mark("'") + tab.scroller.before_jump_requested.emit() if window_text is not None and window_text != tab.search.text: tab.search.clear() diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 31fd9b6a2..bce185298 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -218,9 +218,7 @@ class HintActions: if context.target in [Target.normal, Target.current]: # Set the pre-jump mark ', so we can jump back here after following - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=self._win_id) - tabbed_browser.set_mark("'") + context.tab.scroll.before_jump_requested.emit() try: if context.target == Target.hover: diff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py index 13deb9b6b..10b7d7244 100644 --- a/qutebrowser/components/scrollcommands.py +++ b/qutebrowser/components/scrollcommands.py @@ -94,9 +94,6 @@ def scroll_to_perc(tab: tab.Tab, count: int = None, horizontal: Scroll horizontally instead of vertically. count: Percentage to scroll. """ - # save the pre-jump position in the special ' mark - self.set_mark("'") - if perc is None and count is None: perc = 100 elif count is not None: @@ -109,6 +106,7 @@ def scroll_to_perc(tab: tab.Tab, count: int = None, x = None y = perc + tab.scroller.before_jump_requested.emit() tab.scroller.to_perc(x, y) @@ -120,4 +118,5 @@ def scroll_to_anchor(tab: tab.Tab, name): Args: name: The anchor to scroll to. """ + tab.scroller.before_jump_requested.emit() tab.scroller.to_anchor(name) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 9c6602224..9c14df3ae 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -222,6 +222,7 @@ class TabbedBrowser(QWidget): self._filter.create(self.cur_caret_selection_toggled, tab)) # misc tab.scroller.perc_changed.connect(self.on_scroll_pos_changed) + tab.scroller.before_jump_requested.connect(lambda: self.set_mark("'")) tab.url_changed.connect( functools.partial(self.on_url_changed, tab)) tab.title_changed.connect( @@ -891,8 +892,7 @@ class TabbedBrowser(QWidget): # save the pre-jump position in the special ' mark # this has to happen after we read the mark, otherwise jump_mark # "'" would just jump to the current position every time - self.set_mark("'") - + tab.scroller.before_jump_requested.emit() tab.scroller.to_point(point) else: message.error("Mark {} is not set".format(key)) From bc0ac62087ce0d700ee129467695e92692a6b8cc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:28:29 +0100 Subject: [PATCH 134/258] Rename api.tab.Tab to api.apitypes.Tab Then we can add other related types there, and we don't clash with the common "tab" argument for a new tab. --- qutebrowser/api/{tab.py => apitypes.py} | 0 qutebrowser/components/scrollcommands.py | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename qutebrowser/api/{tab.py => apitypes.py} (100%) diff --git a/qutebrowser/api/tab.py b/qutebrowser/api/apitypes.py similarity index 100% rename from qutebrowser/api/tab.py rename to qutebrowser/api/apitypes.py diff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py index 10b7d7244..c749c5dbe 100644 --- a/qutebrowser/components/scrollcommands.py +++ b/qutebrowser/components/scrollcommands.py @@ -19,13 +19,13 @@ """Scrolling-related commands.""" -from qutebrowser.api import cmdutils, tab +from qutebrowser.api import cmdutils, apitypes @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def scroll_px(tab: tab.Tab, dx: int, dy: int, count: int = 1) -> None: +def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -> None: """Scroll the current tab by 'count * dx/dy' pixels. Args: @@ -43,7 +43,7 @@ def scroll_px(tab: tab.Tab, dx: int, dy: int, count: int = 1) -> None: @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def scroll(tab: tab.Tab, direction: str, count: int = 1) -> None: +def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None: """Scroll the current tab in the given direction. Note you can use `:run-with-count` to have a keybinding with a bigger @@ -82,7 +82,7 @@ def scroll(tab: tab.Tab, direction: str, count: int = 1) -> None: @cmdutils.argument('tab', value=cmdutils.Value.tab) @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('horizontal', flag='x') -def scroll_to_perc(tab: tab.Tab, count: int = None, +def scroll_to_perc(tab: apitypes.Tab, count: int = None, perc: float = None, horizontal: bool = False) -> None: """Scroll to a specific percentage of the page. @@ -112,7 +112,7 @@ def scroll_to_perc(tab: tab.Tab, count: int = None, @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.tab) -def scroll_to_anchor(tab: tab.Tab, name): +def scroll_to_anchor(tab: apitypes.Tab, name): """Scroll to the given anchor in the document. Args: From 7a379ebec17023aa606f314a85e884d354b05ea1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:45:40 +0100 Subject: [PATCH 135/258] Expose KeyMode to extension API --- qutebrowser/api/cmdutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 3a1814856..093244727 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -25,7 +25,7 @@ import typing from qutebrowser.utils import qtutils from qutebrowser.commands import command, cmdexc # pylint: disable=unused-import -from qutebrowser.utils.usertypes import CommandValue as Value +from qutebrowser.utils.usertypes import KeyMode, CommandValue as Value class CommandError(cmdexc.Error): From a09b1a4e05074b0a737cbd7dea80d4b1d1582707 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:32:28 +0100 Subject: [PATCH 136/258] Move caret commands to components.caretcommands --- qutebrowser/api/apitypes.py | 2 +- qutebrowser/app.py | 2 +- qutebrowser/browser/commands.py | 169 ------------------- qutebrowser/components/caretcommands.py | 205 ++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 171 deletions(-) create mode 100644 qutebrowser/components/caretcommands.py diff --git a/qutebrowser/api/apitypes.py b/qutebrowser/api/apitypes.py index 444e49558..8ec7be547 100644 --- a/qutebrowser/api/apitypes.py +++ b/qutebrowser/api/apitypes.py @@ -20,4 +20,4 @@ """A single tab.""" # pylint: disable=unused-import -from qutebrowser.browser.browsertab import AbstractTab as Tab +from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e950ff2ec..0bb7e14cb 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -77,7 +77,7 @@ 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 +from qutebrowser.components import scrollcommands, caretcommands from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds # pylint: enable=unused-import diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 791e45a1a..f1bb8fb35 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1348,18 +1348,6 @@ class CommandDispatcher: except KeyError: raise cmdutils.CommandError("Bookmark '{}' not found!".format(url)) - @cmdutils.register(instance='command-dispatcher', scope='window') - def follow_selected(self, *, tab=False): - """Follow the selected text. - - Args: - tab: Load the selected link in a new tab. - """ - try: - self._current_widget().caret.follow_selected(tab=tab) - except browsertab.WebTabError as e: - raise cmdutils.CommandError(str(e)) - @cmdutils.register(instance='command-dispatcher', name='inspector', scope='window') def toggle_inspector(self): @@ -1800,163 +1788,6 @@ class CommandDispatcher: tab.search.prev_result() tab.search.prev_result(result_cb=cb) - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_next_line(self, count=1): - """Move the cursor or selection to the next line. - - Args: - count: How many lines to move. - """ - self._current_widget().caret.move_to_next_line(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_prev_line(self, count=1): - """Move the cursor or selection to the prev line. - - Args: - count: How many lines to move. - """ - self._current_widget().caret.move_to_prev_line(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_next_char(self, count=1): - """Move the cursor or selection to the next char. - - Args: - count: How many lines to move. - """ - self._current_widget().caret.move_to_next_char(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_prev_char(self, count=1): - """Move the cursor or selection to the previous char. - - Args: - count: How many chars to move. - """ - self._current_widget().caret.move_to_prev_char(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_end_of_word(self, count=1): - """Move the cursor or selection to the end of the word. - - Args: - count: How many words to move. - """ - self._current_widget().caret.move_to_end_of_word(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_next_word(self, count=1): - """Move the cursor or selection to the next word. - - Args: - count: How many words to move. - """ - self._current_widget().caret.move_to_next_word(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_prev_word(self, count=1): - """Move the cursor or selection to the previous word. - - Args: - count: How many words to move. - """ - self._current_widget().caret.move_to_prev_word(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def move_to_start_of_line(self): - """Move the cursor or selection to the start of the line.""" - self._current_widget().caret.move_to_start_of_line() - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def move_to_end_of_line(self): - """Move the cursor or selection to the end of line.""" - self._current_widget().caret.move_to_end_of_line() - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_start_of_next_block(self, count=1): - """Move the cursor or selection to the start of next block. - - Args: - count: How many blocks to move. - """ - self._current_widget().caret.move_to_start_of_next_block(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_start_of_prev_block(self, count=1): - """Move the cursor or selection to the start of previous block. - - Args: - count: How many blocks to move. - """ - self._current_widget().caret.move_to_start_of_prev_block(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_end_of_next_block(self, count=1): - """Move the cursor or selection to the end of next block. - - Args: - count: How many blocks to move. - """ - self._current_widget().caret.move_to_end_of_next_block(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def move_to_end_of_prev_block(self, count=1): - """Move the cursor or selection to the end of previous block. - - Args: - count: How many blocks to move. - """ - self._current_widget().caret.move_to_end_of_prev_block(count) - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def move_to_start_of_document(self): - """Move the cursor or selection to the start of the document.""" - self._current_widget().caret.move_to_start_of_document() - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def move_to_end_of_document(self): - """Move the cursor or selection to the end of the document.""" - self._current_widget().caret.move_to_end_of_document() - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def toggle_selection(self): - """Toggle caret selection mode.""" - self._current_widget().caret.toggle_selection() - - @cmdutils.register(instance='command-dispatcher', modes=[KeyMode.caret], - scope='window') - def drop_selection(self): - """Drop selection and keep selection mode enabled.""" - self._current_widget().caret.drop_selection() - @cmdutils.register(instance='command-dispatcher', scope='window', debug=True) @cmdutils.argument('count', value=cmdutils.Value.count) diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py new file mode 100644 index 000000000..ae0828ef5 --- /dev/null +++ b/qutebrowser/components/caretcommands.py @@ -0,0 +1,205 @@ +# 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 . + +"""Commands related to caret browsing.""" + + +from qutebrowser.api import cmdutils, apitypes + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_next_line(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the next line. + + Args: + count: How many lines to move. + """ + tab.caret.move_to_next_line(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_prev_line(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the prev line. + + Args: + count: How many lines to move. + """ + tab.caret.move_to_prev_line(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_next_char(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the next char. + + Args: + count: How many lines to move. + """ + tab.caret.move_to_next_char(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_prev_char(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the previous char. + + Args: + count: How many chars to move. + """ + tab.caret.move_to_prev_char(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_end_of_word(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the end of the word. + + Args: + count: How many words to move. + """ + tab.caret.move_to_end_of_word(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_next_word(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the next word. + + Args: + count: How many words to move. + """ + tab.caret.move_to_next_word(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_prev_word(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the previous word. + + Args: + count: How many words to move. + """ + tab.caret.move_to_prev_word(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def move_to_start_of_line(tab: apitypes.Tab): + """Move the cursor or selection to the start of the line.""" + tab.caret.move_to_start_of_line() + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def move_to_end_of_line(tab: apitypes.Tab): + """Move the cursor or selection to the end of line.""" + tab.caret.move_to_end_of_line() + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the start of next block. + + Args: + count: How many blocks to move. + """ + tab.caret.move_to_start_of_next_block(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the start of previous block. + + Args: + count: How many blocks to move. + """ + tab.caret.move_to_start_of_prev_block(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the end of next block. + + Args: + count: How many blocks to move. + """ + tab.caret.move_to_end_of_next_block(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1): + """Move the cursor or selection to the end of previous block. + + Args: + count: How many blocks to move. + """ + tab.caret.move_to_end_of_prev_block(count) + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def move_to_start_of_document(tab: apitypes.Tab): + """Move the cursor or selection to the start of the document.""" + tab.caret.move_to_start_of_document() + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def move_to_end_of_document(tab: apitypes.Tab): + """Move the cursor or selection to the end of the document.""" + tab.caret.move_to_end_of_document() + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def toggle_selection(tab: apitypes.Tab): + """Toggle caret selection mode.""" + tab.caret.toggle_selection() + + +@cmdutils.register(modes=[cmdutils.KeyMode.caret]) +def drop_selection(tab: apitypes.Tab): + """Drop selection and keep selection mode enabled.""" + tab.caret.drop_selection() + + +@cmdutils.register() +@cmdutils.argument('tab_obj', value=cmdutils.Value.tab) +def follow_selected(tab_obj: apitypes.Tab, *, tab=False): + """Follow the selected text. + + Args: + tab: Load the selected link in a new tab. + """ + try: + tab_obj.caret.follow_selected(tab=tab) + except apitypes.WebTabError as e: + raise cmdutils.CommandError(str(e)) From da505c84c6dd50e1eecda8baf9bd2d2584efe9cd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:44:49 +0100 Subject: [PATCH 137/258] Expose config.val to extension API --- qutebrowser/api/config.py | 26 ++++++++++++++++++++++++++ qutebrowser/config/configinit.py | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 qutebrowser/api/config.py diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py new file mode 100644 index 000000000..c1394d46a --- /dev/null +++ b/qutebrowser/api/config.py @@ -0,0 +1,26 @@ +# 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 . + +"""Access to the qutebrowser configuration.""" + +import typing + +from qutebrowser.config import config + +val = typing.cast('config.ConfigContainer', None) diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 8480889af..de9651064 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -24,6 +24,7 @@ import sys from PyQt5.QtWidgets import QMessageBox +from qutebrowser.api import config as configapi from qutebrowser.config import (config, configdata, configfiles, configtypes, configexc, configcommands) from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, @@ -44,6 +45,7 @@ def early_init(args): config.instance = config.Config(yaml_config=yaml_config) config.val = config.ConfigContainer(config.instance) + configapi.val = config.ConfigContainer(config.instance) config.key_instance = config.KeyConfig(config.instance) config.cache = configcache.ConfigCache() yaml_config.setParent(config.instance) From ba03e9394aa21c1a6da628dd987709b670668811 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 14:45:10 +0100 Subject: [PATCH 138/258] Move zoom commands to components/ --- qutebrowser/app.py | 2 +- qutebrowser/browser/commands.py | 67 ------------------- qutebrowser/components/zoomcommands.py | 92 ++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 68 deletions(-) create mode 100644 qutebrowser/components/zoomcommands.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 0bb7e14cb..7548c08d0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -77,7 +77,7 @@ 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 +from qutebrowser.components import scrollcommands, caretcommands, zoomcommands from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds # pylint: enable=unused-import diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f1bb8fb35..f6fecdce8 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -793,73 +793,6 @@ class CommandDispatcher: modeman.leave(self._win_id, KeyMode.caret, "yank selected", maybe=True) - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def zoom_in(self, count=1, quiet=False): - """Increase the zoom level for the current tab. - - Args: - count: How many steps to zoom in. - quiet: Don't show a zoom level message. - """ - tab = self._current_widget() - try: - perc = tab.zoom.apply_offset(count) - except ValueError as e: - raise cmdutils.CommandError(e) - if not quiet: - message.info("Zoom level: {}%".format(int(perc)), replace=True) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def zoom_out(self, count=1, quiet=False): - """Decrease the zoom level for the current tab. - - Args: - count: How many steps to zoom out. - quiet: Don't show a zoom level message. - """ - tab = self._current_widget() - try: - perc = tab.zoom.apply_offset(-count) - except ValueError as e: - raise cmdutils.CommandError(e) - if not quiet: - message.info("Zoom level: {}%".format(int(perc)), replace=True) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def zoom(self, zoom=None, count=None, quiet=False): - """Set the zoom level for the current tab. - - The zoom can be given as argument or as [count]. If neither is - given, the zoom is set to the default zoom. If both are given, - use [count]. - - Args: - zoom: The zoom percentage to set. - count: The zoom percentage to set. - quiet: Don't show a zoom level message. - """ - if zoom is not None: - try: - zoom = int(zoom.rstrip('%')) - except ValueError: - raise cmdutils.CommandError("zoom: Invalid int value {}" - .format(zoom)) - - level = count if count is not None else zoom - if level is None: - level = config.val.zoom.default - tab = self._current_widget() - - try: - tab.zoom.set_factor(float(level) / 100) - except ValueError: - raise cmdutils.CommandError("Can't zoom {}%!".format(level)) - if not quiet: - message.info("Zoom level: {}%".format(int(level)), replace=True) - @cmdutils.register(instance='command-dispatcher', scope='window') def tab_only(self, prev=False, next_=False, force=False): """Close all tabs except for the current one. diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py new file mode 100644 index 000000000..c525b771c --- /dev/null +++ b/qutebrowser/components/zoomcommands.py @@ -0,0 +1,92 @@ +# 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 . + +"""Zooming-related commands.""" + +from qutebrowser.api import cmdutils, apitypes, message, config + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def zoom_in(tab: apitypes.Tab, count=1, quiet=False): + """Increase the zoom level for the current tab. + + Args: + count: How many steps to zoom in. + quiet: Don't show a zoom level message. + """ + try: + perc = tab.zoom.apply_offset(count) + except ValueError as e: + raise cmdutils.CommandError(e) + if not quiet: + message.info("Zoom level: {}%".format(int(perc)), replace=True) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def zoom_out(tab: apitypes.Tab, count=1, quiet=False): + """Decrease the zoom level for the current tab. + + Args: + count: How many steps to zoom out. + quiet: Don't show a zoom level message. + """ + try: + perc = tab.zoom.apply_offset(-count) + except ValueError as e: + raise cmdutils.CommandError(e) + if not quiet: + message.info("Zoom level: {}%".format(int(perc)), replace=True) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def zoom(tab: apitypes.Tab, zoom=None, count=None, quiet=False): + """Set the zoom level for the current tab. + + The zoom can be given as argument or as [count]. If neither is + given, the zoom is set to the default zoom. If both are given, + use [count]. + + Args: + zoom: The zoom percentage to set. + count: The zoom percentage to set. + quiet: Don't show a zoom level message. + """ + if zoom is not None: + try: + zoom = int(zoom.rstrip('%')) + except ValueError: + raise cmdutils.CommandError("zoom: Invalid int value {}" + .format(zoom)) + + level = count if count is not None else zoom + if level is None: + level = config.val.zoom.default + + try: + tab.zoom.set_factor(float(level) / 100) + except ValueError: + raise cmdutils.CommandError("Can't zoom {}%!".format(level)) + if not quiet: + message.info("Zoom level: {}%".format(int(level)), replace=True) From 3a7206bda1501f6cb8dd8f3b813e7a8f021e3669 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 15:36:54 +0100 Subject: [PATCH 139/258] Make standarddir.Location private --- qutebrowser/utils/standarddir.py | 46 +++++++++++++++++++------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 356178404..7abb4429d 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -35,9 +35,17 @@ from qutebrowser.utils import log, debug, message, utils _locations = {} -Location = enum.Enum('Location', ['config', 'auto_config', - 'data', 'system_data', - 'cache', 'download', 'runtime']) +class _Location(enum.Enum): + + """A key for _locations.""" + + config = 1 + auto_config = 2 + data = 3 + system_data = 4 + cache = 5 + download = 6 + runtime = 7 APPNAME = 'qutebrowser' @@ -77,8 +85,8 @@ def _init_config(args): else: path = _writable_location(typ) _create(path) - _locations[Location.config] = path - _locations[Location.auto_config] = path + _locations[_Location.config] = path + _locations[_Location.auto_config] = path # Override the normal (non-auto) config on macOS if utils.is_mac: @@ -86,7 +94,7 @@ def _init_config(args): if not overridden: # pragma: no branch path = os.path.expanduser('~/.' + APPNAME) _create(path) - _locations[Location.config] = path + _locations[_Location.config] = path def config(auto=False): @@ -96,8 +104,8 @@ def config(auto=False): which is different on macOS. """ if auto: - return _locations[Location.auto_config] - return _locations[Location.config] + return _locations[_Location.auto_config] + return _locations[_Location.config] def _init_data(args): @@ -115,14 +123,14 @@ def _init_data(args): else: path = _writable_location(typ) _create(path) - _locations[Location.data] = path + _locations[_Location.data] = path # system_data - _locations.pop(Location.system_data, None) # Remove old state + _locations.pop(_Location.system_data, None) # Remove old state if utils.is_linux: path = '/usr/share/' + APPNAME if os.path.exists(path): - _locations[Location.system_data] = path + _locations[_Location.system_data] = path def data(system=False): @@ -133,10 +141,10 @@ def data(system=False): """ if system: try: - return _locations[Location.system_data] + return _locations[_Location.system_data] except KeyError: pass - return _locations[Location.data] + return _locations[_Location.data] def _init_cache(args): @@ -151,11 +159,11 @@ def _init_cache(args): else: path = _writable_location(typ) _create(path) - _locations[Location.cache] = path + _locations[_Location.cache] = path def cache(): - return _locations[Location.cache] + return _locations[_Location.cache] def _init_download(args): @@ -168,11 +176,11 @@ def _init_download(args): overridden, path = _from_args(typ, args) if not overridden: path = _writable_location(typ) - _locations[Location.download] = path + _locations[_Location.download] = path def download(): - return _locations[Location.download] + return _locations[_Location.download] def _init_runtime(args): @@ -205,11 +213,11 @@ def _init_runtime(args): # maximum length of 104 chars), so we don't add the username here... _create(path) - _locations[Location.runtime] = path + _locations[_Location.runtime] = path def runtime(): - return _locations[Location.runtime] + return _locations[_Location.runtime] def _writable_location(typ): From 3abc83d945794584483277e3050be1e4ab4cb16d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 15:43:14 +0100 Subject: [PATCH 140/258] Rename CommandValue.tab to CommandValue.cur_tab --- doc/contributing.asciidoc | 2 +- qutebrowser/commands/command.py | 2 +- qutebrowser/components/caretcommands.py | 24 ++++++++++++------------ qutebrowser/components/scrollcommands.py | 8 ++++---- qutebrowser/components/zoomcommands.py | 6 +++--- qutebrowser/utils/usertypes.py | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index b11014ca1..dc52dd9a0 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -483,7 +483,7 @@ The following arguments are supported for `@cmdutils.argument`: - `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.tab`: The tab object which is currently focused. + - `value=cmdutils.Value.cur_tab`: The tab object which is currently focused. - `completion`: A completion function (see `qutebrowser.completions.models.*`) to use when completing arguments for the given command. - `choices`: The allowed string choices for the argument. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 631ecc354..a91a268bd 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -441,7 +441,7 @@ class Command: self._add_special_arg(value=win_id, param=param, args=args, kwargs=kwargs) continue - elif arg_info.value == usertypes.CommandValue.tab: + elif arg_info.value == usertypes.CommandValue.cur_tab: tab = self._get_objreg(win_id=win_id, name='tab', scope='tab') self._add_special_arg(value=tab, param=param, args=args, kwargs=kwargs) diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index ae0828ef5..c359c27ac 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -24,7 +24,7 @@ from qutebrowser.api import cmdutils, apitypes @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_line(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the next line. @@ -36,7 +36,7 @@ def move_to_next_line(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_line(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the prev line. @@ -48,7 +48,7 @@ def move_to_prev_line(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_char(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the next char. @@ -60,7 +60,7 @@ def move_to_next_char(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_char(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the previous char. @@ -72,7 +72,7 @@ def move_to_prev_char(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_word(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the end of the word. @@ -84,7 +84,7 @@ def move_to_end_of_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_next_word(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the next word. @@ -96,7 +96,7 @@ def move_to_next_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_prev_word(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the previous word. @@ -120,7 +120,7 @@ def move_to_end_of_line(tab: apitypes.Tab): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the start of next block. @@ -132,7 +132,7 @@ def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the start of previous block. @@ -144,7 +144,7 @@ def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the end of next block. @@ -156,7 +156,7 @@ def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1): """Move the cursor or selection to the end of previous block. @@ -192,7 +192,7 @@ def drop_selection(tab: apitypes.Tab): @cmdutils.register() -@cmdutils.argument('tab_obj', value=cmdutils.Value.tab) +@cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab) def follow_selected(tab_obj: apitypes.Tab, *, tab=False): """Follow the selected text. diff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py index c749c5dbe..4a6d9cb99 100644 --- a/qutebrowser/components/scrollcommands.py +++ b/qutebrowser/components/scrollcommands.py @@ -23,7 +23,7 @@ from qutebrowser.api import cmdutils, apitypes @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -> None: """Scroll the current tab by 'count * dx/dy' pixels. @@ -41,7 +41,7 @@ def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -> None: @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None: """Scroll the current tab in the given direction. @@ -79,7 +79,7 @@ def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None: @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('horizontal', flag='x') def scroll_to_perc(tab: apitypes.Tab, count: int = None, @@ -111,7 +111,7 @@ def scroll_to_perc(tab: apitypes.Tab, count: int = None, @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def scroll_to_anchor(tab: apitypes.Tab, name): """Scroll to the given anchor in the document. diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py index c525b771c..16afa18e2 100644 --- a/qutebrowser/components/zoomcommands.py +++ b/qutebrowser/components/zoomcommands.py @@ -23,7 +23,7 @@ from qutebrowser.api import cmdutils, apitypes, message, config @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def zoom_in(tab: apitypes.Tab, count=1, quiet=False): """Increase the zoom level for the current tab. @@ -41,7 +41,7 @@ def zoom_in(tab: apitypes.Tab, count=1, quiet=False): @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def zoom_out(tab: apitypes.Tab, count=1, quiet=False): """Decrease the zoom level for the current tab. @@ -59,7 +59,7 @@ def zoom_out(tab: apitypes.Tab, count=1, quiet=False): @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.tab) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) def zoom(tab: apitypes.Tab, zoom=None, count=None, quiet=False): """Set the zoom level for the current tab. diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index c6fddb039..eb19b9dd3 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -262,7 +262,7 @@ class CommandValue(enum.Enum): count = 1 win_id = 2 - tab = 3 + cur_tab = 3 class Question(QObject): From 2ef82caa107b78aa1d9d4875904a84730e4d4412 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 15:55:05 +0100 Subject: [PATCH 141/258] apitypes: Expose webelem.Error/ClickTarget/JsWorld --- qutebrowser/api/apitypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/api/apitypes.py b/qutebrowser/api/apitypes.py index 8ec7be547..2ed188724 100644 --- a/qutebrowser/api/apitypes.py +++ b/qutebrowser/api/apitypes.py @@ -21,3 +21,5 @@ # pylint: disable=unused-import from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab +from qutebrowser.browser.webelem import Error as WebElemError +from qutebrowser.utils.usertypes import ClickTarget, JsWorld From 7788a91ed290e5be28854de0bdef87c43c769195 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 15:55:50 +0100 Subject: [PATCH 142/258] Add CommandValue.count_tab --- qutebrowser/commands/command.py | 22 ++++++++++++++++++++++ qutebrowser/utils/usertypes.py | 1 + 2 files changed, 23 insertions(+) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index a91a268bd..5e9130b0b 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -26,6 +26,7 @@ import typing import attr +from qutebrowser.api import cmdutils from qutebrowser.commands import cmdexc, argparser from qutebrowser.utils import log, message, docutils, objreg, usertypes, utils from qutebrowser.utils import debug as debug_utils @@ -373,6 +374,22 @@ class Command: raise TypeError("{}: invalid parameter type {} for argument " "{!r}!".format(self.name, param.kind, param.name)) + def _add_count_tab(self, *, win_id, param, args, kwargs): + """Add the count_tab widget argument.""" + tabbed_browser = self._get_objreg( + win_id=win_id, name='tabbed-browser', scope='window') + + if self._count is None: + tab = tabbed_browser.widget.currentWidget() + elif 1 <= self._count <= tabbed_browser.widget.count(): + cmdutils.check_overflow(self._count + 1, 'int') + tab = tabbed_browser.widget.widget(self._count - 1) + else: + tab = None + + self._add_special_arg(value=tab, param=param, args=args, + kwargs=kwargs, optional=True) + def _get_param_value(self, param): """Get the converted value for an inspect.Parameter.""" value = getattr(self.namespace, param.name) @@ -446,6 +463,11 @@ class Command: self._add_special_arg(value=tab, param=param, args=args, kwargs=kwargs) continue + elif arg_info.value == usertypes.CommandValue.count_tab: + self._add_count_tab(win_id=win_id, param=param, args=args, + kwargs=kwargs) + continue + elif arg_info.value is None: pass else: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index eb19b9dd3..c948df48f 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -263,6 +263,7 @@ class CommandValue(enum.Enum): count = 1 win_id = 2 cur_tab = 3 + count_tab = 4 class Question(QObject): From 979be017c3475eed41be70ae663a12609818fca1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 15:56:16 +0100 Subject: [PATCH 143/258] Add components.misccommands --- qutebrowser/app.py | 3 +- qutebrowser/browser/commands.py | 215 ----------------- qutebrowser/components/misccommands.py | 312 +++++++++++++++++++++++++ qutebrowser/misc/utilcmds.py | 74 ------ 4 files changed, 314 insertions(+), 290 deletions(-) create mode 100644 qutebrowser/components/misccommands.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 7548c08d0..6c948e10c 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -77,7 +77,8 @@ 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 +from qutebrowser.components import (scrollcommands, caretcommands, + zoomcommands, misccommands) from qutebrowser.mainwindow.statusbar import command from qutebrowser.misc import utilcmds # pylint: enable=unused-import diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f6fecdce8..19ae41a68 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -19,7 +19,6 @@ """Command dispatcher for TabbedBrowser.""" -import os import os.path import shlex import functools @@ -27,7 +26,6 @@ import typing from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery -from PyQt5.QtPrintSupport import QPrintPreviewDialog from qutebrowser.commands import userscripts, runners from qutebrowser.api import cmdutils @@ -370,83 +368,6 @@ class CommandDispatcher: if parsed is not None: yield parsed - @cmdutils.register(instance='command-dispatcher', name='reload', - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def reloadpage(self, force=False, count=None): - """Reload the current/[count]th tab. - - Args: - count: The tab index to reload, or None. - force: Bypass the page cache. - """ - tab = self._cntwidget(count) - if tab is not None: - tab.reload(force=force) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - def stop(self, count=None): - """Stop loading in the current/[count]th tab. - - Args: - count: The tab index to stop, or None. - """ - tab = self._cntwidget(count) - if tab is not None: - tab.stop() - - def _print_preview(self, tab): - """Show a print preview.""" - def print_callback(ok): - if not ok: - message.error("Printing failed!") - - tab.printing.check_preview_support() - diag = QPrintPreviewDialog(tab) - diag.setAttribute(Qt.WA_DeleteOnClose) - diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint | - Qt.WindowMinimizeButtonHint) - diag.paintRequested.connect(functools.partial( - tab.printing.to_printer, callback=print_callback)) - diag.exec_() - - def _print_pdf(self, tab, filename): - """Print to the given PDF file.""" - tab.printing.check_pdf_support() - filename = os.path.expanduser(filename) - directory = os.path.dirname(filename) - if directory and not os.path.exists(directory): - os.mkdir(directory) - tab.printing.to_pdf(filename) - log.misc.debug("Print to file: {}".format(filename)) - - @cmdutils.register(instance='command-dispatcher', name='print', - scope='window') - @cmdutils.argument('count', value=cmdutils.Value.count) - @cmdutils.argument('pdf', flag='f', metavar='file') - def printpage(self, preview=False, count=None, *, pdf=None): - """Print the current/[count]th tab. - - Args: - preview: Show preview instead of printing. - count: The tab index to print, or None. - pdf: The file path to write the PDF to. - """ - tab = self._cntwidget(count) - if tab is None: - return - - try: - if preview: - self._print_preview(tab) - elif pdf: - self._print_pdf(tab, pdf) - else: - tab.printing.show_dialog() - except browsertab.WebTabError as e: - raise cmdutils.CommandError(e) - @cmdutils.register(instance='command-dispatcher', scope='window') def tab_clone(self, bg=False, window=False): """Duplicate the current tab. @@ -1110,11 +1031,6 @@ class CommandDispatcher: proc.start(cmd, args) proc.finished.connect(_on_proc_finished) - @cmdutils.register(instance='command-dispatcher', scope='window') - def home(self): - """Open main startpage in current tab.""" - self.openurl(config.val.url.start_pages[0]) - def _run_userscript(self, selection, cmd, args, verbose, count): """Run a userscript given as argument. @@ -1384,30 +1300,6 @@ class CommandDispatcher: else: tab.action.show_source(pygments) - @cmdutils.register(instance='command-dispatcher', scope='window', - debug=True) - def debug_dump_page(self, dest, plain=False): - """Dump the current page's content to a file. - - Args: - dest: Where to write the file to. - plain: Write plain text instead of HTML. - """ - tab = self._current_widget() - dest = os.path.expanduser(dest) - - def callback(data): - """Write the data to disk.""" - try: - with open(dest, 'w', encoding='utf-8') as f: - f.write(data) - except OSError as e: - message.error('Could not write page: {}'.format(e)) - else: - message.info("Dumped page to {}.".format(dest)) - - tab.dump_async(callback, plain=plain) - @cmdutils.register(instance='command-dispatcher', scope='window') def history(self, tab=True, bg=False, window=False): """Show browsing history. @@ -1525,75 +1417,6 @@ class CommandDispatcher: message.error(str(e)) ed.backup() - @cmdutils.register(instance='command-dispatcher', maxsplit=0, - scope='window') - def insert_text(self, text): - """Insert text at cursor position. - - Args: - text: The text to insert. - """ - tab = self._current_widget() - - def _insert_text_cb(elem): - if elem is None: - message.error("No element focused!") - return - try: - elem.insert_text(text) - except webelem.Error as e: - message.error(str(e)) - return - - tab.elements.find_focused(_insert_text_cb) - - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('filter_', choices=['id']) - def click_element(self, filter_: str, value: str, *, - target: usertypes.ClickTarget = - usertypes.ClickTarget.normal, - force_event: bool = False) -> None: - """Click the element matching the given filter. - - The given filter needs to result in exactly one element, otherwise, an - error is shown. - - Args: - filter_: How to filter the elements. - id: Get an element based on its ID. - value: The value to filter for. - target: How to open the clicked element (normal/tab/tab-bg/window). - force_event: Force generating a fake click event. - """ - tab = self._current_widget() - - def single_cb(elem): - """Click a single element.""" - if elem is None: - message.error("No element found with id {}!".format(value)) - return - try: - elem.click(target, force_event=force_event) - except webelem.Error as e: - message.error(str(e)) - return - - # def multiple_cb(elems): - # """Click multiple elements (with only one expected).""" - # if not elems: - # message.error("No element found!") - # return - # elif len(elems) != 1: - # message.error("{} elements found!".format(len(elems))) - # return - # elems[0].click(target) - - handlers = { - 'id': (tab.elements.find_id, single_cb), - } - handler, callback = handlers[filter_] - handler(value, callback) - def _search_cb(self, found, *, tab, old_scroll_pos, options, text, prev): """Callback called from search/search_next/search_prev. @@ -1721,27 +1544,6 @@ class CommandDispatcher: tab.search.prev_result() tab.search.prev_result(result_cb=cb) - @cmdutils.register(instance='command-dispatcher', scope='window', - debug=True) - @cmdutils.argument('count', value=cmdutils.Value.count) - def debug_webaction(self, action, count=1): - """Execute a webaction. - - Available actions: - http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) - http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) - - Args: - action: The action to execute, e.g. MoveToNextChar. - count: How many times to repeat the action. - """ - tab = self._current_widget() - for _ in range(count): - try: - tab.action.run_string(action) - except browsertab.WebTabError as e: - raise cmdutils.CommandError(str(e)) - @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *, @@ -1920,20 +1722,3 @@ class CommandDispatcher: window = self._tabbed_browser.widget.window() window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) - - @cmdutils.register(instance='command-dispatcher', scope='window', - name='tab-mute') - @cmdutils.argument('count', value=cmdutils.Value.count) - def tab_mute(self, count=None): - """Mute/Unmute the current/[count]th tab. - - Args: - count: The tab index to mute or unmute, or None - """ - tab = self._cntwidget(count) - if tab is None: - return - try: - tab.audio.set_muted(tab.audio.is_muted(), override=True) - except browsertab.WebTabError as e: - raise cmdutils.CommandError(e) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py new file mode 100644 index 000000000..4f730af3c --- /dev/null +++ b/qutebrowser/components/misccommands.py @@ -0,0 +1,312 @@ +# 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 . + +"""Various commands.""" + +import os +import signal +import functools +import typing +import logging + +try: + import hunter +except ImportError: + hunter = None + +from qutebrowser.api import cmdutils, apitypes, message, config + +from PyQt5.QtCore import Qt +from PyQt5.QtPrintSupport import QPrintPreviewDialog + + +@cmdutils.register(name='reload') +@cmdutils.argument('count', value=cmdutils.Value.count) +@cmdutils.argument('tab', value=cmdutils.Value.count_tab) +def reloadpage(tab, force=False, count=None): + """Reload the current/[count]th tab. + + Args: + count: The tab index to reload, or None. + force: Bypass the page cache. + """ + if tab is not None: + tab.reload(force=force) + + +@cmdutils.register() +@cmdutils.argument('count', value=cmdutils.Value.count) +@cmdutils.argument('tab', value=cmdutils.Value.count_tab) +def stop(tab, count=None): + """Stop loading in the current/[count]th tab. + + Args: + count: The tab index to stop, or None. + """ + if tab is not None: + tab.stop() + + +def _print_preview(tab): + """Show a print preview.""" + def print_callback(ok): + if not ok: + message.error("Printing failed!") + + tab.printing.check_preview_support() + diag = QPrintPreviewDialog(tab) + diag.setAttribute(Qt.WA_DeleteOnClose) + diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint) + diag.paintRequested.connect(functools.partial( + tab.printing.to_printer, callback=print_callback)) + diag.exec_() + + +def _print_pdf(tab, filename): + """Print to the given PDF file.""" + tab.printing.check_pdf_support() + filename = os.path.expanduser(filename) + directory = os.path.dirname(filename) + if directory and not os.path.exists(directory): + os.mkdir(directory) + tab.printing.to_pdf(filename) + logging.getLogger('misc').debug("Print to file: {}".format(filename)) + + +@cmdutils.register(name='print') +@cmdutils.argument('tab', value=cmdutils.Value.count_tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +@cmdutils.argument('pdf', flag='f', metavar='file') +def printpage(tab, preview=False, count=None, *, pdf=None): + """Print the current/[count]th tab. + + Args: + preview: Show preview instead of printing. + count: The tab index to print, or None. + pdf: The file path to write the PDF to. + """ + if tab is None: + return + + try: + if preview: + _print_preview(tab) + elif pdf: + _print_pdf(tab, pdf) + else: + tab.printing.show_dialog() + except apitypes.WebTabError as e: + raise cmdutils.CommandError(e) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +def home(tab): + """Open main startpage in current tab.""" + tab.load_url(config.val.url.start_pages[0]) + + +@cmdutils.register(debug=True) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +def debug_dump_page(tab, dest, plain=False): + """Dump the current page's content to a file. + + Args: + dest: Where to write the file to. + plain: Write plain text instead of HTML. + """ + dest = os.path.expanduser(dest) + + def callback(data): + """Write the data to disk.""" + try: + with open(dest, 'w', encoding='utf-8') as f: + f.write(data) + except OSError as e: + message.error('Could not write page: {}'.format(e)) + else: + message.info("Dumped page to {}.".format(dest)) + + tab.dump_async(callback, plain=plain) + + +@cmdutils.register(maxsplit=0) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +def insert_text(tab, text): + """Insert text at cursor position. + + Args: + text: The text to insert. + """ + def _insert_text_cb(elem): + if elem is None: + message.error("No element focused!") + return + try: + elem.insert_text(text) + except apitypes.WebElemError as e: + message.error(str(e)) + return + + tab.elements.find_focused(_insert_text_cb) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +@cmdutils.argument('filter_', choices=['id']) +def click_element(tab, filter_: str, value: str, *, + target: apitypes.ClickTarget = + apitypes.ClickTarget.normal, + force_event: bool = False) -> None: + """Click the element matching the given filter. + + The given filter needs to result in exactly one element, otherwise, an + error is shown. + + Args: + filter_: How to filter the elements. + id: Get an element based on its ID. + value: The value to filter for. + target: How to open the clicked element (normal/tab/tab-bg/window). + force_event: Force generating a fake click event. + """ + def single_cb(elem): + """Click a single element.""" + if elem is None: + message.error("No element found with id {}!".format(value)) + return + try: + elem.click(target, force_event=force_event) + except apitypes.WebElemError as e: + message.error(str(e)) + return + + handlers = { + 'id': (tab.elements.find_id, single_cb), + } + handler, callback = handlers[filter_] + handler(value, callback) + + +@cmdutils.register(debug=True) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def debug_webaction(tab, action, count=1): + """Execute a webaction. + + Available actions: + http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) + http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) + + Args: + action: The action to execute, e.g. MoveToNextChar. + count: How many times to repeat the action. + """ + for _ in range(count): + try: + tab.action.run_string(action) + except apitypes.WebTabError as e: + raise cmdutils.CommandError(str(e)) + + +@cmdutils.register() +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +@cmdutils.argument('count', value=cmdutils.Value.count) +def tab_mute(tab, count=None): + """Mute/Unmute the current/[count]th tab. + + Args: + count: The tab index to mute or unmute, or None + """ + if tab is None: + return + try: + tab.audio.set_muted(tab.audio.is_muted(), override=True) + except apitypes.WebTabError as e: + raise cmdutils.CommandError(e) + + +@cmdutils.register() +def nop(): + """Do nothing.""" + + +@cmdutils.register() +def message_error(text): + """Show an error message in the statusbar. + + Args: + text: The text to show. + """ + message.error(text) + + +@cmdutils.register() +@cmdutils.argument('count', value=cmdutils.Value.count) +def message_info(text, count=1): + """Show an info message in the statusbar. + + Args: + text: The text to show. + count: How many times to show the message + """ + for _ in range(count): + message.info(text) + + +@cmdutils.register() +def message_warning(text): + """Show a warning message in the statusbar. + + Args: + text: The text to show. + """ + message.warning(text) + + +@cmdutils.register(debug=True) +@cmdutils.argument('typ', choices=['exception', 'segfault']) +def debug_crash(typ='exception'): + """Crash for debugging purposes. + + Args: + typ: either 'exception' or 'segfault'. + """ + if typ == 'segfault': + os.kill(os.getpid(), signal.SIGSEGV) + raise Exception("Segfault failed (wat.)") + else: + raise Exception("Forced crash") + + +@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True) +def debug_trace(expr=""): + """Trace executed code via hunter. + + Args: + expr: What to trace, passed to hunter. + """ + if hunter is None: + raise cmdutils.CommandError("You need to install 'hunter' to use this " + "command!") + try: + eval('hunter.trace({})'.format(expr)) + except Exception as e: + raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 287704551..7c515cb0e 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -24,11 +24,6 @@ import os import signal import traceback -try: - import hunter -except ImportError: - hunter = None - from PyQt5.QtCore import QUrl # so it's available for :debug-pyeval from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import @@ -111,60 +106,12 @@ def run_with_count(count_arg: int, command: str, win_id: int, runners.CommandRunner(win_id).run(command, count_arg * count) -@cmdutils.register() -def message_error(text): - """Show an error message in the statusbar. - - Args: - text: The text to show. - """ - message.error(text) - - -@cmdutils.register() -@cmdutils.argument('count', value=cmdutils.Value.count) -def message_info(text, count=1): - """Show an info message in the statusbar. - - Args: - text: The text to show. - count: How many times to show the message - """ - for _ in range(count): - message.info(text) - - -@cmdutils.register() -def message_warning(text): - """Show a warning message in the statusbar. - - Args: - text: The text to show. - """ - message.warning(text) - - @cmdutils.register() def clear_messages(): """Clear all message notifications.""" message.global_bridge.clear_messages.emit() -@cmdutils.register(debug=True) -@cmdutils.argument('typ', choices=['exception', 'segfault']) -def debug_crash(typ='exception'): - """Crash for debugging purposes. - - Args: - typ: either 'exception' or 'segfault'. - """ - if typ == 'segfault': - os.kill(os.getpid(), signal.SIGSEGV) - raise Exception("Segfault failed (wat.)") - else: - raise Exception("Forced crash") - - @cmdutils.register(debug=True) def debug_all_objects(): """Print a list of all objects to the debug log.""" @@ -220,22 +167,6 @@ def debug_console(): con_widget.show() -@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True) -def debug_trace(expr=""): - """Trace executed code via hunter. - - Args: - expr: What to trace, passed to hunter. - """ - if hunter is None: - raise cmdutils.CommandError("You need to install 'hunter' to use this " - "command!") - try: - eval('hunter.trace({})'.format(expr)) - except Exception as e: - raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e)) - - @cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) def debug_pyeval(s, file=False, quiet=False): """Evaluate a python string and display the results as a web page. @@ -371,11 +302,6 @@ def window_only(current_win_id): window.close() -@cmdutils.register() -def nop(): - """Do nothing.""" - - @cmdutils.register() @cmdutils.argument('win_id', value=cmdutils.Value.win_id) def version(win_id, paste=False): From bfa518896a695f0545dedb708f4e6db5667a9dd8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:23:34 +0100 Subject: [PATCH 144/258] Fix Command.takes_count Before, what this actually did was checking the .count argument of a string (the *keys* in self._qute_args). Therefore, it always returned True as soon as a command had any @cmdutils.argument decorator. --- qutebrowser/commands/command.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 5e9130b0b..0904abf95 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -537,7 +537,10 @@ class Command: def takes_count(self): """Return true iff this command can take a count argument.""" - return any(arg.count for arg in self._qute_args) + count_values = [usertypes.CommandValue.count, + usertypes.CommandValue.count_tab] + return any(info.value in count_values + for info in self._qute_args.values()) def register(self): """Register this command in objects.commands.""" From 40d376fbcfc0a2599342b0550b0a82d80c2ae7f4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:25:12 +0100 Subject: [PATCH 145/258] Move unit tests --- tests/unit/components/test_misccommands.py | 93 ++++++++++++++++++++++ tests/unit/misc/test_utilcmds.py | 68 +--------------- 2 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 tests/unit/components/test_misccommands.py diff --git a/tests/unit/components/test_misccommands.py b/tests/unit/components/test_misccommands.py new file mode 100644 index 000000000..95eb0c6e3 --- /dev/null +++ b/tests/unit/components/test_misccommands.py @@ -0,0 +1,93 @@ +# 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 . + +"""Tests for qutebrowser.components.misccommands.""" + +import signal +import contextlib +import time + +import pytest + +from qutebrowser.api import cmdutils +from qutebrowser.utils import utils +from qutebrowser.components import misccommands + + +@contextlib.contextmanager +def _trapped_segv(handler): + """Temporarily install given signal handler for SIGSEGV.""" + old_handler = signal.signal(signal.SIGSEGV, handler) + yield + signal.signal(signal.SIGSEGV, old_handler) + + +def test_debug_crash_exception(): + """Verify that debug_crash crashes as intended.""" + with pytest.raises(Exception, match="Forced crash"): + misccommands.debug_crash(typ='exception') + + +@pytest.mark.skipif(utils.is_windows, + reason="current CPython/win can't recover from SIGSEGV") +def test_debug_crash_segfault(): + """Verify that debug_crash crashes as intended.""" + caught = False + + def _handler(num, frame): + """Temporary handler for segfault.""" + nonlocal caught + caught = num == signal.SIGSEGV + + with _trapped_segv(_handler): + # since we handle the segfault, execution will continue and run into + # the "Segfault failed (wat.)" Exception + with pytest.raises(Exception, match="Segfault failed"): + misccommands.debug_crash(typ='segfault') + time.sleep(0.001) + assert caught + + +def test_debug_trace(mocker): + """Check if hunter.trace is properly called.""" + # but only if hunter is available + pytest.importorskip('hunter') + hunter_mock = mocker.patch.object(misccommands, 'hunter') + misccommands.debug_trace(1) + hunter_mock.trace.assert_called_with(1) + + +def test_debug_trace_exception(mocker): + """Check that exceptions thrown by hunter.trace are handled.""" + def _mock_exception(): + """Side effect for testing debug_trace's reraise.""" + raise Exception('message') + + hunter_mock = mocker.patch.object(misccommands, 'hunter') + hunter_mock.trace.side_effect = _mock_exception + with pytest.raises(cmdutils.CommandError, match='Exception: message'): + misccommands.debug_trace() + + +def test_debug_trace_no_hunter(monkeypatch): + """Test that an error is shown if debug_trace is called without hunter.""" + monkeypatch.setattr(misccommands, 'hunter', None) + with pytest.raises(cmdutils.CommandError, match="You need to install " + "'hunter' to use this command!"): + misccommands.debug_trace() diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index b4af06aff..1b71d5ddc 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -19,80 +19,14 @@ """Tests for qutebrowser.misc.utilcmds.""" -import contextlib import logging -import signal -import time import pytest from PyQt5.QtCore import QUrl from qutebrowser.misc import utilcmds from qutebrowser.api import cmdutils -from qutebrowser.utils import utils, objreg - - -@contextlib.contextmanager -def _trapped_segv(handler): - """Temporarily install given signal handler for SIGSEGV.""" - old_handler = signal.signal(signal.SIGSEGV, handler) - yield - signal.signal(signal.SIGSEGV, old_handler) - - -def test_debug_crash_exception(): - """Verify that debug_crash crashes as intended.""" - with pytest.raises(Exception, match="Forced crash"): - utilcmds.debug_crash(typ='exception') - - -@pytest.mark.skipif(utils.is_windows, - reason="current CPython/win can't recover from SIGSEGV") -def test_debug_crash_segfault(): - """Verify that debug_crash crashes as intended.""" - caught = False - - def _handler(num, frame): - """Temporary handler for segfault.""" - nonlocal caught - caught = num == signal.SIGSEGV - - with _trapped_segv(_handler): - # since we handle the segfault, execution will continue and run into - # the "Segfault failed (wat.)" Exception - with pytest.raises(Exception, match="Segfault failed"): - utilcmds.debug_crash(typ='segfault') - time.sleep(0.001) - assert caught - - -def test_debug_trace(mocker): - """Check if hunter.trace is properly called.""" - # but only if hunter is available - pytest.importorskip('hunter') - hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') - utilcmds.debug_trace(1) - hunter_mock.trace.assert_called_with(1) - - -def test_debug_trace_exception(mocker): - """Check that exceptions thrown by hunter.trace are handled.""" - def _mock_exception(): - """Side effect for testing debug_trace's reraise.""" - raise Exception('message') - - hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') - hunter_mock.trace.side_effect = _mock_exception - with pytest.raises(cmdutils.CommandError, match='Exception: message'): - utilcmds.debug_trace() - - -def test_debug_trace_no_hunter(monkeypatch): - """Test that an error is shown if debug_trace is called without hunter.""" - monkeypatch.setattr(utilcmds, 'hunter', None) - with pytest.raises(cmdutils.CommandError, match="You need to install " - "'hunter' to use this command!"): - utilcmds.debug_trace() +from qutebrowser.utils import objreg def test_repeat_command_initial(mocker, mode_manager): From 34aaca2aa0870190ccdea3192132a09ff90066cf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:25:19 +0100 Subject: [PATCH 146/258] Fix lint --- qutebrowser/api/config.py | 5 ++++- qutebrowser/browser/hints.py | 2 +- qutebrowser/components/misccommands.py | 17 ++++++----------- qutebrowser/components/zoomcommands.py | 15 ++++++++------- qutebrowser/misc/utilcmds.py | 1 - 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index c1394d46a..6558cf42a 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -21,6 +21,9 @@ import typing -from qutebrowser.config import config +MYPY = False +if MYPY: + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config import config val = typing.cast('config.ConfigContainer', None) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index bce185298..43257d0a8 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -218,7 +218,7 @@ class HintActions: if context.target in [Target.normal, Target.current]: # Set the pre-jump mark ', so we can jump back here after following - context.tab.scroll.before_jump_requested.emit() + context.tab.scroller.before_jump_requested.emit() try: if context.target == Target.hover: diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 4f730af3c..af10dc707 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -22,7 +22,6 @@ import os import signal import functools -import typing import logging try: @@ -30,16 +29,15 @@ try: except ImportError: hunter = None -from qutebrowser.api import cmdutils, apitypes, message, config - from PyQt5.QtCore import Qt from PyQt5.QtPrintSupport import QPrintPreviewDialog +from qutebrowser.api import cmdutils, apitypes, message, config + @cmdutils.register(name='reload') -@cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def reloadpage(tab, force=False, count=None): +def reloadpage(tab, force=False): """Reload the current/[count]th tab. Args: @@ -51,9 +49,8 @@ def reloadpage(tab, force=False, count=None): @cmdutils.register() -@cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def stop(tab, count=None): +def stop(tab): """Stop loading in the current/[count]th tab. Args: @@ -92,9 +89,8 @@ def _print_pdf(tab, filename): @cmdutils.register(name='print') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -@cmdutils.argument('count', value=cmdutils.Value.count) @cmdutils.argument('pdf', flag='f', metavar='file') -def printpage(tab, preview=False, count=None, *, pdf=None): +def printpage(tab, preview=False, *, pdf=None): """Print the current/[count]th tab. Args: @@ -228,8 +224,7 @@ def debug_webaction(tab, action, count=1): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -@cmdutils.argument('count', value=cmdutils.Value.count) -def tab_mute(tab, count=None): +def tab_mute(tab): """Mute/Unmute the current/[count]th tab. Args: diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py index 16afa18e2..05d2d8481 100644 --- a/qutebrowser/components/zoomcommands.py +++ b/qutebrowser/components/zoomcommands.py @@ -61,7 +61,7 @@ def zoom_out(tab: apitypes.Tab, count=1, quiet=False): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def zoom(tab: apitypes.Tab, zoom=None, count=None, quiet=False): +def zoom(tab: apitypes.Tab, level=None, count=None, quiet=False): """Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is @@ -69,19 +69,20 @@ def zoom(tab: apitypes.Tab, zoom=None, count=None, quiet=False): use [count]. Args: - zoom: The zoom percentage to set. + level: The zoom percentage to set. count: The zoom percentage to set. quiet: Don't show a zoom level message. """ - if zoom is not None: + if level is not None: try: - zoom = int(zoom.rstrip('%')) + level = int(level.rstrip('%')) except ValueError: raise cmdutils.CommandError("zoom: Invalid int value {}" - .format(zoom)) + .format(level)) - level = count if count is not None else zoom - if level is None: + if count is not None: + level = count + elif level is None: level = config.val.zoom.default try: diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 7c515cb0e..c2b2e6168 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -21,7 +21,6 @@ import functools import os -import signal import traceback from PyQt5.QtCore import QUrl From 23d0dbd998e8252e250131a0c1699f5c61ddc494 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:28:43 +0100 Subject: [PATCH 147/258] Fix handling of optional values in _add_special_arg That way, we can pass None as a valid value. --- qutebrowser/commands/command.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 0904abf95..67e54ccb7 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -350,7 +350,7 @@ class Command: raise ValueError("Invalid scope {}!".format(scope)) return objreg.get(name, scope=scope, window=win_id, tab=tab_id) - def _add_special_arg(self, *, value, param, args, kwargs, optional=False): + def _add_special_arg(self, *, value, param, args, kwargs): """Add a special argument value to a function call. Arguments: @@ -358,18 +358,11 @@ class Command: param: The parameter being filled. args: The positional argument list. Gets modified directly. kwargs: The keyword argument dict. Gets modified directly. - optional: Whether the value can be optional. """ - if not optional: - assert value is not None if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - if value is not None: - args.append(value) - else: - args.append(param.default) + args.append(value) elif param.kind == inspect.Parameter.KEYWORD_ONLY: - if value is not None: - kwargs[param.name] = value + kwargs[param.name] = value else: raise TypeError("{}: invalid parameter type {} for argument " "{!r}!".format(self.name, param.kind, param.name)) @@ -388,7 +381,7 @@ class Command: tab = None self._add_special_arg(value=tab, param=param, args=args, - kwargs=kwargs, optional=True) + kwargs=kwargs) def _get_param_value(self, param): """Get the converted value for an inspect.Parameter.""" @@ -451,8 +444,13 @@ class Command: args=args, kwargs=kwargs) continue elif arg_info.value == usertypes.CommandValue.count: - self._add_special_arg(value=self._count, param=param, - args=args, kwargs=kwargs, optional=True) + if self._count is None: + assert param.default is not inspect.Parameter.empty + value = param.default + else: + value = self._count + self._add_special_arg(value=value, param=param, + args=args, kwargs=kwargs) continue elif arg_info.value == usertypes.CommandValue.win_id: self._add_special_arg(value=win_id, param=param, From a0dca95310e061eb594bd6e986a7a0039fe192cd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:35:40 +0100 Subject: [PATCH 148/258] Move Command._handle_special_call_arg out of _get_call_args --- qutebrowser/commands/command.py | 87 ++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 67e54ccb7..2191769bb 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -420,6 +420,55 @@ class Command: return value + def _handle_special_call_arg(self, *, pos, param, win_id, args, kwargs): + """Check whether the argument is special, and if so, fill it in. + + Args: + pos: The position of the argument. + param: The argparse.Parameter. + win_id: The window ID the command is run in. + args/kwargs: The args/kwargs to fill. + + Return: + True if it was a special arg, False otherwise. + """ + arg_info = self.get_arg_info(param) + if pos == 0 and self._instance is not None: + assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + self_value = self._get_objreg(win_id=win_id, name=self._instance, + scope=self._scope) + self._add_special_arg(value=self_value, param=param, + args=args, kwargs=kwargs) + return True + elif arg_info.value == usertypes.CommandValue.count: + if self._count is None: + assert param.default is not inspect.Parameter.empty + value = param.default + else: + value = self._count + self._add_special_arg(value=value, param=param, + args=args, kwargs=kwargs) + return True + elif arg_info.value == usertypes.CommandValue.win_id: + self._add_special_arg(value=win_id, param=param, + args=args, kwargs=kwargs) + return True + elif arg_info.value == usertypes.CommandValue.cur_tab: + tab = self._get_objreg(win_id=win_id, name='tab', scope='tab') + self._add_special_arg(value=tab, param=param, + args=args, kwargs=kwargs) + return True + elif arg_info.value == usertypes.CommandValue.count_tab: + self._add_count_tab(win_id=win_id, param=param, args=args, + kwargs=kwargs) + return True + elif arg_info.value is None: + pass + else: + raise utils.Unreachable(arg_info) + + return False + def _get_call_args(self, win_id): """Get arguments for a function call. @@ -434,42 +483,10 @@ class Command: signature = inspect.signature(self.handler) for i, param in enumerate(signature.parameters.values()): - arg_info = self.get_arg_info(param) - if i == 0 and self._instance is not None: - assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD - self_value = self._get_objreg(win_id=win_id, - name=self._instance, - scope=self._scope) - self._add_special_arg(value=self_value, param=param, - args=args, kwargs=kwargs) + if self._handle_special_call_arg(pos=i, param=param, + win_id=win_id, args=args, + kwargs=kwargs): continue - elif arg_info.value == usertypes.CommandValue.count: - if self._count is None: - assert param.default is not inspect.Parameter.empty - value = param.default - else: - value = self._count - self._add_special_arg(value=value, param=param, - args=args, kwargs=kwargs) - continue - elif arg_info.value == usertypes.CommandValue.win_id: - self._add_special_arg(value=win_id, param=param, - args=args, kwargs=kwargs) - continue - elif arg_info.value == usertypes.CommandValue.cur_tab: - tab = self._get_objreg(win_id=win_id, name='tab', scope='tab') - self._add_special_arg(value=tab, param=param, - args=args, kwargs=kwargs) - continue - elif arg_info.value == usertypes.CommandValue.count_tab: - self._add_count_tab(win_id=win_id, param=param, args=args, - kwargs=kwargs) - continue - - elif arg_info.value is None: - pass - else: - raise utils.Unreachable(arg_info) value = self._get_param_value(param) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: From d484d9363e28adf11713724645f2784b254cae69 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:51:55 +0100 Subject: [PATCH 149/258] Fix caret cmds --- qutebrowser/components/caretcommands.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index c359c27ac..98791e921 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -168,24 +168,28 @@ def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def move_to_start_of_document(tab: apitypes.Tab): """Move the cursor or selection to the start of the document.""" tab.caret.move_to_start_of_document() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def move_to_end_of_document(tab: apitypes.Tab): """Move the cursor or selection to the end of the document.""" tab.caret.move_to_end_of_document() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def toggle_selection(tab: apitypes.Tab): """Toggle caret selection mode.""" tab.caret.toggle_selection() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def drop_selection(tab: apitypes.Tab): """Drop selection and keep selection mode enabled.""" tab.caret.drop_selection() From a7a8f4470b89fc184fc22990e2c335540d44aa1e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:57:48 +0100 Subject: [PATCH 150/258] Make ExternalEditor slots private --- qutebrowser/misc/editor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 038331c9b..3c5b7471e 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -84,7 +84,7 @@ class ExternalEditor(QObject): message.error("Failed to delete tempfile... ({})".format(e)) @pyqtSlot(int, QProcess.ExitStatus) - def on_proc_closed(self, _exitcode, exitstatus): + def _on_proc_closed(self, _exitcode, exitstatus): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. @@ -100,7 +100,7 @@ class ExternalEditor(QObject): self._cleanup() @pyqtSlot(QProcess.ProcessError) - def on_proc_error(self, _err): + def _on_proc_error(self, _err): self._cleanup() def edit(self, text, caret_position=None): @@ -176,8 +176,8 @@ class ExternalEditor(QObject): column: the column number to pass to the editor """ self._proc = guiprocess.GUIProcess(what='editor', parent=self) - self._proc.finished.connect(self.on_proc_closed) - self._proc.error.connect(self.on_proc_error) + self._proc.finished.connect(self._on_proc_closed) + self._proc.error.connect(self._on_proc_error) editor = config.val.editor.command executable = editor[0] From 4d8c8a5d92d3d4abbbb2f03e5790c24beed03e65 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 30 Nov 2018 16:58:33 +0100 Subject: [PATCH 151/258] Fix :home with pinned tabs --- qutebrowser/components/misccommands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index af10dc707..9ecaa999c 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -116,7 +116,10 @@ def printpage(tab, preview=False, *, pdf=None): @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def home(tab): """Open main startpage in current tab.""" - tab.load_url(config.val.url.start_pages[0]) + if tab.data.pinned: + message.info("Tab is pinned!") + else: + tab.load_url(config.val.url.start_pages[0]) @cmdutils.register(debug=True) From 0e547414af8771d939aaf5d41ac1cdd25648c258 Mon Sep 17 00:00:00 2001 From: Pellegrino Prevete Date: Sun, 2 Dec 2018 05:32:56 +0000 Subject: [PATCH 152/258] Update translations and desktop actions Added translations for most languages and added some "standard" desktop actions. --- misc/qutebrowser.desktop | 163 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/misc/qutebrowser.desktop b/misc/qutebrowser.desktop index 5243b0c17..976803915 100644 --- a/misc/qutebrowser.desktop +++ b/misc/qutebrowser.desktop @@ -1,7 +1,45 @@ [Desktop Entry] Name=qutebrowser GenericName=Web Browser +GenericName[ar]=ﻢﺘﺼﻔﺣ ﺎﻠﺸﺒﻛﺓ +GenericName[bg]=Уеб браузър +GenericName[ca]=Navegador web +GenericName[cs]=WWW prohlížeč +GenericName[da]=Browser +GenericName[de]=Web-Browser +GenericName[el]=Περιηγητής ιστού +GenericName[en_GB]=Web Browser +GenericName[es]=Navegador web +GenericName[et]=Veebibrauser +GenericName[fi]=WWW-selain +GenericName[fr]=Navigateur Web +GenericName[gu]=વેબ બ્રાઉઝર +GenericName[he]=דפדפן אינטרנט +GenericName[hi]=वेब ब्राउज़र +GenericName[hu]=Webböngésző +GenericName[it]=Browser Web +GenericName[ja]=ウェブブラウザ +GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ +GenericName[ko]=웹 브라우저 +GenericName[lt]=Žiniatinklio naršyklė +GenericName[lv]=Tīmekļa pārlūks +GenericName[ml]=വെബ് ബ്രൌസര്<200d> +GenericName[mr]=वेब ब्राऊजर +GenericName[nb]=Nettleser +GenericName[nl]=Webbrowser +GenericName[pl]=Przeglądarka WWW +GenericName[pt]=Navegador Web +GenericName[pt_BR]=Navegador da Internet +GenericName[ro]=Navigator de Internet +GenericName[ru]=Веб-браузер +GenericName[sl]=Spletni brskalnik +GenericName[sv]=Webbläsare +GenericName[ta]=இணைய உலாவி +GenericName[th]=เว็บเบราว์เซอร์ +GenericName[tr]=Web Tarayıcı +GenericName[uk]=Навігатор Тенет瀏覽器 Comment=A keyboard-driven, vim-like browser based on PyQt5 +Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5 Icon=qutebrowser Type=Application Categories=Network;WebBrowser; @@ -10,3 +48,128 @@ Terminal=false StartupNotify=false MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute; Keywords=Browser + +[Desktop Action new-window] +Name=New Window +Name[am]=አዲስ መስኮት +Name[ar]=ﻥﺎﻓﺫﺓ ﺝﺪﻳﺩﺓ +Name[bg]=Нов прозорец +Name[bn]=নতুন উইন্ডো +Name[ca]=Finestra nova +Name[cs]=Nové okno +Name[da]=Nyt vindue +Name[de]=Neues Fenster +Name[el]=Νέο Παράθυρο +Name[en_GB]=New Window +Name[es]=Nueva ventana +Name[et]=Uus aken +Name[fa]=پﻦﺟﺮﻫ ﺝﺩیﺩ +Name[fi]=Uusi ikkuna +Name[fil]=New Window +Name[fr]=Nouvelle fenêtre +Name[gu]=નવી વિંડો +Name[hi]=नई विंडो +Name[hr]=Novi prozor +Name[hu]=Új ablak +Name[id]=Jendela Baru +Name[it]=Nuova finestra +Name[iw]=חלון חדש +Name[ja]=新規ウインドウ +Name[kn]=ಹೊಸ ವಿಂಡೊ +Name[ko]=새 창 +Name[lt]=Naujas langas +Name[lv]=Jauns logs +Name[ml]=പുതിയ വിന്<200d>ഡോ +Name[mr]=नवीन विंडो +Name[nl]=Nieuw venster +Name[no]=Nytt vindu +Name[pl]=Nowe okno +Name[pt]=Nova janela +Name[pt_BR]=Nova janela +Name[ro]=Fereastră nouă +Name[ru]=Новое окно +Name[sk]=Nové okno +Name[sl]=Novo okno +Name[sr]=Нови прозор +Name[sv]=Nytt fönster +Name[sw]=Dirisha Jipya +Name[ta]=புதிய சாளரம் +Name[te]=క్రొత్త విండో +Name[th]=หน้าต่างใหม่ +Name[tr]=Yeni Pencere +Name[uk]=Нове вікно +Name[vi]=Cửa sổ Mới +Exec=qutebrowser + +[Desktop Action preferences] +Name=Preferences +Name[an]=Preferencias +Name[ar]=ﺎﻠﺘﻔﻀﻳﻼﺗ +Name[as]=পছন্দসমূহ +Name[be]=Настройкі +Name[bg]=Настройки +Name[bn_IN]=পছন্দ +Name[bs]=Postavke +Name[ca]=Preferències +Name[ca@valencia]=Preferències +Name[cs]=Předvolby +Name[da]=Indstillinger +Name[de]=Einstellungen +Name[el]=Προτιμήσεις +Name[en_GB]=Preferences +Name[eo]=Agordoj +Name[es]=Preferencias +Name[et]=Eelistused +Name[eu]=Hobespenak +Name[fa]=ﺕﺮﺟیﺡﺎﺗ +Name[fi]=Asetukset +Name[fr]=Préférences +Name[fur]=Preferencis +Name[ga]=Sainroghanna +Name[gd]=Roghainnean +Name[gl]=Preferencias +Name[gu]=પસંદગીઓ +Name[he]=העדפות +Name[hi]=वरीयताएँ +Name[hr]=Osobitosti +Name[hu]=Beállítások +Name[id]=Preferensi +Name[is]=Kjörstillingar +Name[it]=Preferenze +Name[ja]=設定 +Name[kk]=Баптаулар +Name[km]=ចំណូលចិត្ត +Name[kn]=ಆದ್ಯತೆಗಳು +Name[ko]=기본 설정 +Name[lt]=Nuostatos +Name[lv]=Iestatījumi +Name[ml]=മുന്<200d>ഗണനകള്<200d> +Name[mr]=पसंती +Name[nb]=Brukervalg +Name[ne]=प्राथमिकताहरू +Name[nl]=Voorkeuren +Name[oc]=Preferéncias +Name[or]=ପସନ୍ଦ +Name[pa]=ਮੇਰੀ ਪਸੰਦ +Name[pl]=Preferencje +Name[pt]=Preferências +Name[pt_BR]=Preferências +Name[ro]=Preferințe +Name[ru]=Параметры +Name[sk]=Nastavenia +Name[sl]=Možnosti +Name[sr]=Поставке +Name[sr@latin]=Postavke +Name[sv]=Inställningar +Name[ta]=விருப்பங்கள் +Name[te]=అభీష్టాలు +Name[tg]=Хусусиятҳо +Name[th]=ปรับแต่ง +Name[tr]=Tercihler +Name[ug]=ﻡﺎﻳﻰﻠﻟﻰﻗ +Name[uk]=Параметри +Name[vi]=Tùy thích +Name[zh_CN]=首选项 +Name[zh_HK]=偏好設定 +Name[zh_TW]=偏好設定 +Exec=qutebrowser "qute://settings" From e81f070a7929e6c7a1f4ff097c7106a5d256d90e Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sun, 2 Dec 2018 22:56:44 +0700 Subject: [PATCH 153/258] Use tojson filter to convert url to Javascript string Fixes #4450. --- qutebrowser/html/error.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/html/error.html b/qutebrowser/html/error.html index b3e6f576e..e03ddad67 100644 --- a/qutebrowser/html/error.html +++ b/qutebrowser/html/error.html @@ -61,7 +61,7 @@ li { {{ super() }} function tryagain() { - location.href = "{{ url }}"; + location.href = {{ url|tojson }}; } {% endblock %} From ef5e5d84ac3d196ca9af400fcc4294e643970ec7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:44:10 +0100 Subject: [PATCH 154/258] Add assert for Command.pos_args --- qutebrowser/commands/command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 2191769bb..4d0d4af92 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -298,6 +298,8 @@ class Command: name = argparser.arg_name(param.name) arg_info = self.get_arg_info(param) + assert not arg_info.value, name + if arg_info.flag is not None: shortname = arg_info.flag else: From ede13c76d9cbae4134119bc05e60ba0d0930e598 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:44:35 +0100 Subject: [PATCH 155/258] Fix checking for count in src2asciidoc --- qutebrowser/commands/command.py | 8 +++++--- scripts/dev/src2asciidoc.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 4d0d4af92..8d2f5ad57 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -71,6 +71,10 @@ class Command: _scope: The scope to get _instance for in the object registry. """ + # CommandValue values which need a count + COUNT_COMMAND_VALUES = [usertypes.CommandValue.count, + usertypes.CommandValue.count_tab] + def __init__(self, *, handler, name, instance=None, maxsplit=None, modes=None, not_modes=None, debug=False, deprecated=False, no_cmd_split=False, star_args_optional=False, scope='global', @@ -554,9 +558,7 @@ class Command: def takes_count(self): """Return true iff this command can take a count argument.""" - count_values = [usertypes.CommandValue.count, - usertypes.CommandValue.count_tab] - return any(info.value in count_values + return any(info.value in self.COUNT_COMMAND_VALUES for info in self._qute_args.values()) def register(self): diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index ad1397d82..f7b1d46cd 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -252,7 +252,7 @@ def _get_command_doc_count(cmd, parser): Strings which should be added to the docs. """ for param in inspect.signature(cmd.handler).parameters.values(): - if cmd.get_arg_info(param).count: + if cmd.get_arg_info(param).value in cmd.COUNT_COMMAND_VALUES: yield "" yield "==== count" try: From 285db344b34368a45380e6f00d87f7764bb008a4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:44:51 +0100 Subject: [PATCH 156/258] Allow to document count_tab as "count:" argument --- scripts/dev/src2asciidoc.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index f7b1d46cd..ba4e9b69c 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -257,9 +257,13 @@ def _get_command_doc_count(cmd, parser): yield "==== count" try: yield parser.arg_descs[param.name] - except KeyError as e: - raise KeyError("No description for count arg {!r} of command " - "{!r}!".format(param.name, cmd.name)) from e + except KeyError: + try: + yield parser.arg_descs['count'] + except KeyError as e: + raise KeyError("No description for count arg {!r} of " + "command {!r}!" + .format(param.name, cmd.name)) from e def _get_command_doc_notes(cmd): From a47ee08a84beb91f152dc483701f2526a7859ab0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:45:13 +0100 Subject: [PATCH 157/258] Fix wrong/missing argument annotations --- qutebrowser/components/caretcommands.py | 2 ++ qutebrowser/components/misccommands.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index 98791e921..fe04f2483 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -108,12 +108,14 @@ def move_to_prev_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def move_to_start_of_line(tab: apitypes.Tab): """Move the cursor or selection to the start of the line.""" tab.caret.move_to_start_of_line() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) +@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def move_to_end_of_line(tab: apitypes.Tab): """Move the cursor or selection to the end of line.""" tab.caret.move_to_end_of_line() diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 9ecaa999c..2f9e2f5e5 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -226,7 +226,7 @@ def debug_webaction(tab, action, count=1): @cmdutils.register() -@cmdutils.argument('tab', value=cmdutils.Value.cur_tab) +@cmdutils.argument('tab', value=cmdutils.Value.count_tab) def tab_mute(tab): """Mute/Unmute the current/[count]th tab. From d01672bab78114e71a47b3bdff40be4afbb96e08 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:45:25 +0100 Subject: [PATCH 158/258] Regenerate docs --- doc/help/commands.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 13cf0a790..2d71a28c1 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1484,14 +1484,14 @@ Yank something to the clipboard or primary selection. [[zoom]] === zoom -Syntax: +:zoom [*--quiet*] ['zoom']+ +Syntax: +:zoom [*--quiet*] ['level']+ Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is given, the zoom is set to the default zoom. If both are given, use [count]. ==== positional arguments -* +'zoom'+: The zoom percentage to set. +* +'level'+: The zoom percentage to set. ==== optional arguments * +*-q*+, +*--quiet*+: Don't show a zoom level message. From b0ae4deac8a6e1d9af03a318c86c4b9de83b7c48 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 08:49:10 +0100 Subject: [PATCH 159/258] ADjust check_coverage for api/ --- qutebrowser/api/config.py | 2 +- scripts/dev/check_coverage.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index 6558cf42a..d45703242 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -22,7 +22,7 @@ import typing MYPY = False -if MYPY: +if MYPY: # pragma: no cover # pylint: disable=unused-import,useless-suppression from qutebrowser.config import config diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 8d0647b0d..62c0b5142 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -53,11 +53,18 @@ MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file') PERFECT_FILES = [ (None, 'commands/cmdexc.py'), - ('tests/unit/api/test_cmdutils.py', - 'api/cmdutils.py'), ('tests/unit/commands/test_argparser.py', 'commands/argparser.py'), + ('tests/unit/api/test_cmdutils.py', + 'api/cmdutils.py'), + (None, + 'api/apitypes.py'), + (None, + 'api/config.py'), + (None, + 'api/message.py'), + ('tests/unit/browser/webkit/test_cache.py', 'browser/webkit/cache.py'), ('tests/unit/browser/webkit/test_cookies.py', From 208d3db475cf5e5fd50840dd80bfef0fc85e98f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 12:14:33 +0100 Subject: [PATCH 160/258] Add types for most of qutebrowser.config --- mypy.ini | 36 ++++++ qutebrowser/config/config.py | 184 +++++++++++++++++---------- qutebrowser/config/configcommands.py | 97 ++++++++------ qutebrowser/config/configdata.py | 59 ++++++--- qutebrowser/config/configdiff.py | 7 +- qutebrowser/config/configexc.py | 35 +++-- qutebrowser/config/configfiles.py | 114 ++++++++++------- qutebrowser/config/configinit.py | 20 +-- qutebrowser/config/configutils.py | 49 ++++--- 9 files changed, 385 insertions(+), 216 deletions(-) diff --git a/mypy.ini b/mypy.ini index 288cc6515..be5424327 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,3 +61,39 @@ disallow_incomplete_defs = True [mypy-qutebrowser.commands.cmdutils] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.config.config] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcache] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcommands] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdata] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdiff] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configexc] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configfiles] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configinit] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configutils] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1d7e34345..44bf3ca77 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,18 +23,21 @@ import copy import contextlib import functools import typing +from typing import Any -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl from qutebrowser.config import configdata, configexc, configutils -from qutebrowser.utils import utils, log, jinja +from qutebrowser.utils import utils, log, jinja, urlmatch from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: - # pylint: disable=unused-import - from qutebrowser.config import configcache # pragma: no cover +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from typing import Tuple, MutableMapping + from qutebrowser.config import configcache, configfiles + from qutebrowser.misc import savemanager # An easy way to access the config from other code via config.val.foo val = typing.cast('ConfigContainer', None) @@ -61,7 +64,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name _function: Whether a function rather than a method is decorated. """ - def __init__(self, option, function=False): + def __init__(self, option: str, function: bool = False) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -74,7 +77,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name self._function = function change_filters.append(self) - def validate(self): + def validate(self) -> None: """Make sure the configured option or prefix exists. We can't do this in __init__ as configdata isn't ready yet. @@ -83,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): + 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. @@ -96,7 +99,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func): + def __call__(self, func: typing.Callable) -> typing.Callable: """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -114,20 +117,21 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def wrapper(option=None): + def func_wrapper(option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func() return None + return func_wrapper else: @functools.wraps(func) - def wrapper(wrapper_self, option=None): + def meth_wrapper(wrapper_self: typing.Any, + option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func(wrapper_self) return None - - return wrapper + return meth_wrapper class KeyConfig: @@ -140,17 +144,22 @@ class KeyConfig: _config: The Config object to be used. """ - def __init__(self, config): + _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]] + + def __init__(self, config: 'Config') -> None: self._config = config - def _validate(self, key, mode): + def _validate(self, key: keyutils.KeySequence, mode: str) -> None: """Validate the given key and mode.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - def get_bindings_for(self, mode): + def get_bindings_for( + self, + mode: str + ) -> typing.Dict[keyutils.KeySequence, str]: """Get the combined bindings for the given mode.""" bindings = dict(val.bindings.default[mode]) for key, binding in val.bindings.commands[mode].items(): @@ -160,9 +169,9 @@ class KeyConfig: bindings[key] = binding return bindings - def get_reverse_bindings_for(self, mode): + def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': """Get a dict of commands to a list of bindings for the mode.""" - cmd_to_keys = {} + cmd_to_keys = {} # type: KeyConfig._ReverseBindings bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): @@ -175,7 +184,10 @@ class KeyConfig: cmd_to_keys[cmd].insert(0, str(seq)) return cmd_to_keys - def get_command(self, key, mode, default=False): + def get_command(self, + key: keyutils.KeySequence, + mode: str, + default: bool = False) -> str: """Get the command for a given key (or None).""" self._validate(key, mode) if default: @@ -184,7 +196,11 @@ class KeyConfig: bindings = self.get_bindings_for(mode) return bindings.get(key, None) - def bind(self, key, command, *, mode, save_yaml=False): + def bind(self, + key: keyutils.KeySequence, + command: str, *, + mode: str, + save_yaml: bool = False) -> None: """Add a new binding from key to command.""" if command is not None and not command.strip(): raise configexc.KeybindingError( @@ -192,8 +208,8 @@ class KeyConfig: 'mode'.format(key, mode)) self._validate(key, mode) - log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( - key, command, mode)) + log.keyboard.vdebug( # type: ignore + "Adding binding {} -> {} in mode {}.".format(key, command, mode)) bindings = self._config.get_mutable_obj('bindings.commands') if mode not in bindings: @@ -201,7 +217,10 @@ class KeyConfig: bindings[mode][str(key)] = command self._config.update_mutables(save_yaml=save_yaml) - def bind_default(self, key, *, mode='normal', save_yaml=False): + def bind_default(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Restore a default keybinding.""" self._validate(key, mode) @@ -213,7 +232,10 @@ class KeyConfig: "Can't find binding '{}' in {} mode".format(key, mode)) self._config.update_mutables(save_yaml=save_yaml) - def unbind(self, key, *, mode='normal', save_yaml=False): + def unbind(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Unbind the given key in the given mode.""" self._validate(key, mode) @@ -254,24 +276,27 @@ class Config(QObject): MUTABLE_TYPES = (dict, list) changed = pyqtSignal(str) - def __init__(self, yaml_config, parent=None): + def __init__(self, + yaml_config: 'configfiles.YamlConfig', + parent: QObject = None) -> None: super().__init__(parent) self.changed.connect(_render_stylesheet.cache_clear) - self._mutables = {} + self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] self._yaml = yaml_config self._init_values() - def _init_values(self): + def _init_values(self) -> None: """Populate the self._values dict.""" - self._values = {} + self._values = {} # type: typing.Mapping for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -279,7 +304,10 @@ class Config(QObject): """ self._yaml.init_save_manager(save_manager) - def _set_value(self, opt, value, pattern=None): + def _set_value(self, + opt: 'configdata.Option', + value: Any, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: @@ -294,12 +322,12 @@ class Config(QObject): log.config.debug("Config option changed: {} = {}".format( opt.name, value)) - def _check_yaml(self, opt, save_yaml): + def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None: """Make sure the given option may be set in autoconfig.yml.""" if save_yaml and opt.no_autoconfig: raise configexc.NoAutoconfigError(opt.name) - def read_yaml(self): + def read_yaml(self) -> None: """Read the YAML settings from self._yaml.""" self._yaml.load() for values in self._yaml: @@ -307,7 +335,7 @@ class Config(QObject): self._set_value(values.opt, scoped.value, pattern=scoped.pattern) - def get_opt(self, name): + def get_opt(self, name: str) -> 'configdata.Option': """Get a configdata.Option object for the given setting.""" try: return configdata.DATA[name] @@ -318,7 +346,10 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, name, url=None, *, fallback=True): + def get(self, + name: str, + url: QUrl = None, *, + fallback: bool = True) -> Any: """Get the given setting converted for Python code. Args: @@ -328,7 +359,7 @@ class Config(QObject): obj = self.get_obj(name, url=url, fallback=fallback) return opt.typ.to_py(obj) - def _maybe_copy(self, value): + def _maybe_copy(self, value: Any) -> Any: """Copy the value if it could potentially be mutated.""" if isinstance(value, self.MUTABLE_TYPES): # For mutable objects, create a copy so we don't accidentally @@ -339,7 +370,10 @@ class Config(QObject): assert value.__hash__ is not None, value return value - def get_obj(self, name, *, url=None, fallback=True): + def get_obj(self, + name: str, *, + url: QUrl = None, + fallback: bool = True) -> Any: """Get the given setting as object (for YAML/config.py). Note that the returned values are not watched for mutation. @@ -349,7 +383,10 @@ class Config(QObject): value = self._values[name].get_for_url(url, fallback=fallback) return self._maybe_copy(value) - def get_obj_for_pattern(self, name, *, pattern): + def get_obj_for_pattern( + self, name: str, *, + pattern: typing.Optional[urlmatch.UrlPattern] + ) -> Any: """Get the given setting as object (for YAML/config.py). This gets the overridden value for a given pattern, or @@ -359,7 +396,8 @@ class Config(QObject): value = self._values[name].get_for_pattern(pattern, fallback=False) return self._maybe_copy(value) - def get_mutable_obj(self, name, *, pattern=None): + def get_mutable_obj(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> Any: """Get an object which can be mutated, e.g. in a config.py. If a pattern is given, return the value for that pattern. @@ -384,7 +422,8 @@ class Config(QObject): return copy_value - def get_str(self, name, *, pattern=None): + def get_str(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> str: """Get the given setting as string. If a pattern is given, get the setting for the given pattern or @@ -395,7 +434,10 @@ class Config(QObject): value = values.get_for_pattern(pattern) return opt.typ.to_str(value) - def set_obj(self, name, value, *, pattern=None, save_yaml=False): + def set_obj(self, name: str, + value: Any, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. @@ -406,7 +448,10 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, value, pattern=pattern) - def set_str(self, name, value, *, pattern=None, save_yaml=False): + def set_str(self, name: str, + value: str, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a string. If save_yaml=True is given, store the new value to YAML. @@ -421,7 +466,9 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, converted, pattern=pattern) - def unset(self, name, *, save_yaml=False, pattern=None): + def unset(self, name: str, *, + save_yaml: bool = False, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting back to its default.""" opt = self.get_opt(name) self._check_yaml(opt, save_yaml) @@ -432,7 +479,7 @@ class Config(QObject): if save_yaml: self._yaml.unset(name, pattern=pattern) - def clear(self, *, save_yaml=False): + def clear(self, *, save_yaml: bool = False) -> None: """Clear all settings in the config. If save_yaml=True is given, also remove all customization from the YAML @@ -446,7 +493,7 @@ class Config(QObject): if save_yaml: self._yaml.clear() - def update_mutables(self, *, save_yaml=False): + def update_mutables(self, *, save_yaml: bool = False) -> None: """Update mutable settings if they changed. Every time someone calls get_obj() on a mutable object, we save a @@ -461,7 +508,7 @@ class Config(QObject): self.set_obj(name, new_value, save_yaml=save_yaml) self._mutables = {} - def dump_userconfig(self): + def dump_userconfig(self) -> str: """Get the part of the config which was changed by the user. Return: @@ -490,7 +537,10 @@ class ConfigContainer: _pattern: The URL pattern to be used. """ - def __init__(self, config, configapi=None, prefix='', pattern=None): + def __init__(self, config: Config, + configapi: 'configfiles.ConfigAPI' = None, + prefix: str = '', + pattern: urlmatch.UrlPattern = None) -> None: self._config = config self._prefix = prefix self._configapi = configapi @@ -498,13 +548,13 @@ class ConfigContainer: if configapi is None and pattern is not None: raise TypeError("Can't use pattern without configapi!") - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, constructor=True, config=self._config, configapi=self._configapi, prefix=self._prefix, pattern=self._pattern) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: try: yield except configexc.Error as e: @@ -513,7 +563,7 @@ class ConfigContainer: text = "While {} '{}'".format(action, name) self._configapi.errors.append(configexc.ConfigErrorDesc(text, e)) - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: """Get an option or a new ConfigContainer with the added prefix. If we get an option which exists, we return the value for it. @@ -540,7 +590,7 @@ class ConfigContainer: return self._config.get_mutable_obj( name, pattern=self._pattern) - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value: Any) -> None: """Set the given option in the config.""" if attr.startswith('_'): super().__setattr__(attr, value) @@ -550,7 +600,7 @@ class ConfigContainer: with self._handle_error('setting', name): self._config.set_obj(name, value, pattern=self._pattern) - def _join(self, attr): + def _join(self, attr: str) -> str: """Get the prefix joined with the given attribute.""" if self._prefix: return '{}.{}'.format(self._prefix, attr) @@ -558,8 +608,10 @@ class ConfigContainer: return attr -def set_register_stylesheet(obj, *, stylesheet=None, update=True): - """Set the stylesheet for an object based on it's STYLESHEET attribute. +def set_register_stylesheet(obj: QObject, *, + stylesheet: str = None, + update: bool = True) -> None: + """Set the stylesheet for an object. Also, register an update when the config is changed. @@ -574,7 +626,7 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True): @functools.lru_cache() -def _render_stylesheet(stylesheet): +def _render_stylesheet(stylesheet: str) -> str: """Render the given stylesheet jinja template.""" with jinja.environment.no_autoescape(): template = jinja.environment.from_string(stylesheet) @@ -590,7 +642,9 @@ class StyleSheetObserver(QObject): _stylesheet: The stylesheet template to use. """ - def __init__(self, obj, stylesheet, update): + def __init__(self, obj: QObject, + stylesheet: typing.Optional[str], + update: bool) -> None: super().__init__() self._obj = obj self._update = update @@ -599,11 +653,11 @@ class StyleSheetObserver(QObject): if self._update: self.setParent(self._obj) if stylesheet is None: - self._stylesheet = obj.STYLESHEET + self._stylesheet = obj.STYLESHEET # type: str else: self._stylesheet = stylesheet - def _get_stylesheet(self): + def _get_stylesheet(self) -> str: """Format a stylesheet based on a template. Return: @@ -612,19 +666,15 @@ class StyleSheetObserver(QObject): return _render_stylesheet(self._stylesheet) @pyqtSlot() - def _update_stylesheet(self): + def _update_stylesheet(self) -> None: """Update the stylesheet for obj.""" self._obj.setStyleSheet(self._get_stylesheet()) - def register(self): - """Do a first update and listen for more. - - Args: - update: if False, don't listen for future updates. - """ + def register(self) -> None: + """Do a first update and listen for more.""" qss = self._get_stylesheet() - log.config.vdebug("stylesheet for {}: {}".format( - self._obj.__class__.__name__, qss)) + log.config.vdebug( # type: ignore + "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) self._obj.setStyleSheet(qss) if self._update: instance.changed.connect(self._update_stylesheet) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 574bc06af..0ee77fcc9 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -19,6 +19,7 @@ """Commands related to the configuration.""" +import typing import os.path import contextlib @@ -31,24 +32,34 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config.config import Config, KeyConfig + class ConfigCommands: """qutebrowser commands related to the configuration.""" - def __init__(self, config, keyconfig): + def __init__(self, + config: 'Config', + keyconfig: 'KeyConfig') -> None: self._config = config self._keyconfig = keyconfig @contextlib.contextmanager - def _handle_config_error(self): + def _handle_config_error(self) -> typing.Iterator[None]: """Catch errors in set_command and raise CommandError.""" try: yield except configexc.Error as e: raise cmdutils.CommandError(str(e)) - def _parse_pattern(self, pattern): + def _parse_pattern( + self, + pattern: typing.Optional[str] + ) -> typing.Optional[urlmatch.UrlPattern]: """Parse a pattern string argument to a pattern.""" if pattern is None: return None @@ -59,14 +70,15 @@ class ConfigCommands: raise cmdutils.CommandError("Error while parsing {}: {}" .format(pattern, str(e))) - def _parse_key(self, key): + def _parse_key(self, key: str) -> keyutils.KeySequence: """Parse a key argument.""" try: return keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: raise cmdutils.CommandError(str(e)) - def _print_value(self, option, pattern): + def _print_value(self, option: str, + pattern: typing.Optional[urlmatch.UrlPattern]) -> None: """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) @@ -81,8 +93,9 @@ class ConfigCommands: @cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('pattern', flag='u') - def set(self, win_id, option=None, value=None, temp=False, print_=False, - *, pattern=None): + def set(self, win_id: int, option: str = None, value: str = None, + temp: bool = False, print_: bool = False, + *, pattern: str = None) -> None: """Set an option. If the option name ends with '?' or no value is provided, the @@ -108,28 +121,28 @@ class ConfigCommands: raise cmdutils.CommandError("Toggling values was moved to the " ":config-cycle command") - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) if option.endswith('?') and option != '?': - self._print_value(option[:-1], pattern=pattern) + self._print_value(option[:-1], pattern=parsed_pattern) return with self._handle_config_error(): if value is None: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) else: - self._config.set_str(option, value, pattern=pattern, + self._config.set_str(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) - def bind(self, win_id, key=None, command=None, *, mode='normal', - default=False): + def bind(self, win_id: str, key: str = None, command: str = None, *, + mode: str = 'normal', default: bool = False) -> None: """Bind a key to a command. If no command is given, show the current binding for the given key. @@ -174,7 +187,7 @@ class ConfigCommands: self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands') - def unbind(self, key, *, mode='normal'): + def unbind(self, key: str, *, mode: str = 'normal') -> None: """Unbind a keychain. Args: @@ -191,8 +204,9 @@ class ConfigCommands: @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('pattern', flag='u') - def config_cycle(self, option, *values, pattern=None, temp=False, - print_=False): + def config_cycle(self, option: str, *values: str, + pattern: str = None, + temp: bool = False, print_: bool = False) -> None: """Cycle an option between multiple values. Args: @@ -202,15 +216,15 @@ class ConfigCommands: temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) with self._handle_config_error(): opt = self._config.get_opt(option) - old_value = self._config.get_obj_for_pattern(option, - pattern=pattern) + old_value = self._config.get_obj_for_pattern( + option, pattern=parsed_pattern) if not values and isinstance(opt.typ, configtypes.Bool): - values = ['true', 'false'] + values = ('true', 'false') if len(values) < 2: raise cmdutils.CommandError("Need at least two values for " @@ -219,25 +233,25 @@ class ConfigCommands: # Use the next valid value from values, or the first if the current # value does not appear in the list with self._handle_config_error(): - values = [opt.typ.from_str(val) for val in values] + cycle_values = [opt.typ.from_str(val) for val in values] try: - idx = values.index(old_value) - idx = (idx + 1) % len(values) - value = values[idx] + idx = cycle_values.index(old_value) + idx = (idx + 1) % len(cycle_values) + value = cycle_values[idx] except ValueError: - value = values[0] + value = cycle_values[0] with self._handle_config_error(): - self._config.set_obj(option, value, pattern=pattern, + self._config.set_obj(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.customized_option) - def config_unset(self, option, temp=False): + def config_unset(self, option: str, temp: bool = False) -> None: """Unset an option. This sets an option back to its default and removes it from @@ -252,7 +266,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_add(self, option, value, temp=False): + def config_list_add(self, option: str, value: str, + temp: bool = False) -> None: """Append a value to a config option that is a list. Args: @@ -273,7 +288,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_add(self, option, key, value, temp=False, replace=False): + def config_dict_add(self, option: str, key: str, value: str, + temp: bool = False, replace: bool = False) -> None: """Add a key/value pair to a dictionary option. Args: @@ -302,7 +318,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_remove(self, option, value, temp=False): + def config_list_remove(self, option: str, value: str, + temp: bool = False) -> None: """Remove a value from a list. Args: @@ -329,7 +346,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_remove(self, option, key, temp=False): + def config_dict_remove(self, option: str, key: str, + temp: bool = False) -> None: """Remove a key from a dict. Args: @@ -354,7 +372,7 @@ class ConfigCommands: self._config.update_mutables(save_yaml=not temp) @cmdutils.register(instance='config-commands') - def config_clear(self, save=False): + def config_clear(self, save: bool = False) -> None: """Set all settings back to their default. Args: @@ -364,7 +382,7 @@ class ConfigCommands: self._config.clear(save_yaml=save) @cmdutils.register(instance='config-commands') - def config_source(self, filename=None, clear=False): + def config_source(self, filename: str = None, clear: bool = False) -> None: """Read a config.py file. Args: @@ -386,13 +404,13 @@ class ConfigCommands: raise cmdutils.CommandError(e) @cmdutils.register(instance='config-commands') - def config_edit(self, no_source=False): + def config_edit(self, no_source: bool = False) -> None: """Open the config.py file in the editor. Args: no_source: Don't re-source the config file after editing. """ - def on_file_updated(): + def on_file_updated() -> None: """Source the new config when editing finished. This can't use cmdutils.CommandError as it's run async. @@ -410,7 +428,8 @@ class ConfigCommands: ed.edit_file(filename) @cmdutils.register(instance='config-commands') - def config_write_py(self, filename=None, force=False, defaults=False): + def config_write_py(self, filename: str = None, + force: bool = False, defaults: bool = False) -> None: """Write the current configuration to a config.py file. Args: @@ -429,13 +448,13 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) + options = [] # type: typing.List if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] bindings = dict(configdata.DATA['bindings.default'].default) commented = True else: - options = [] for values in self._config: for scoped in values: options.append((scoped.pattern, values.opt, scoped.value)) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index dace0772a..c93032387 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -24,14 +24,18 @@ Module attributes: DATA: A dict of Option objects after init() has been called. """ +import typing +from typing import Optional # pylint: disable=unused-import import functools import attr from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils -DATA = None -MIGRATIONS = None +DATA = typing.cast(typing.Mapping[str, 'Option'], None) +MIGRATIONS = typing.cast('Migrations', None) + +_BackendDict = typing.Mapping[str, typing.Union[str, bool]] @attr.s @@ -42,15 +46,15 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name = attr.ib() - typ = attr.ib() - default = attr.ib() - backends = attr.ib() - raw_backends = attr.ib() - description = attr.ib() - supports_pattern = attr.ib(default=False) - restart = attr.ib(default=False) - no_autoconfig = attr.ib(default=False) + name = attr.ib() # type: str + typ = attr.ib() # type: configtypes.BaseType + default = attr.ib() # type: typing.Any + backends = attr.ib() # type: typing.Iterable[usertypes.Backend] + raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]] + description = attr.ib() # type: str + supports_pattern = attr.ib(default=False) # type: bool + restart = attr.ib(default=False) # type: bool + no_autoconfig = attr.ib(default=False) # type: bool @attr.s @@ -63,11 +67,13 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed = attr.ib(default=attr.Factory(dict)) - deleted = attr.ib(default=attr.Factory(list)) + renamed = attr.ib( + default=attr.Factory(dict)) # type: typing.Dict[str, str] + deleted = attr.ib( + default=attr.Factory(list)) # type: typing.List[str] -def _raise_invalid_node(name, what, node): +def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: """Raise an exception for an invalid configdata YAML node. Args: @@ -79,13 +85,16 @@ def _raise_invalid_node(name, what, node): name, what, node)) -def _parse_yaml_type(name, node): +def _parse_yaml_type( + name: str, + node: typing.Union[str, typing.Mapping[str, typing.Any]], +) -> configtypes.BaseType: if isinstance(node, str): # e.g: # type: Bool # -> create the type object without any arguments type_name = node - kwargs = {} + kwargs = {} # type: typing.MutableMapping[str, typing.Any] elif isinstance(node, dict): # e.g: # type: @@ -123,7 +132,10 @@ def _parse_yaml_type(name, node): type_name, node, e)) -def _parse_yaml_backends_dict(name, node): +def _parse_yaml_backends_dict( + name: str, + node: _BackendDict, +) -> typing.Sequence[usertypes.Backend]: """Parse a dict definition for backends. Example: @@ -160,7 +172,10 @@ def _parse_yaml_backends_dict(name, node): return backends -def _parse_yaml_backends(name, node): +def _parse_yaml_backends( + name: str, + node: typing.Union[None, str, _BackendDict], +) -> typing.Sequence[usertypes.Backend]: """Parse a backend node in the yaml. It can have one of those four forms: @@ -187,7 +202,9 @@ def _parse_yaml_backends(name, node): raise utils.Unreachable -def _read_yaml(yaml_data): +def _read_yaml( + yaml_data: str, +) -> typing.Tuple[typing.Mapping[str, Option], Migrations]: """Read config data from a YAML file. Args: @@ -249,12 +266,12 @@ def _read_yaml(yaml_data): @functools.lru_cache(maxsize=256) -def is_valid_prefix(prefix): +def is_valid_prefix(prefix: str) -> bool: """Check whether the given prefix is a valid prefix for some option.""" return any(key.startswith(prefix + '.') for key in DATA) -def init(): +def init() -> None: """Initialize configdata from the YAML file.""" global DATA, MIGRATIONS DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml')) diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 9f8b70a26..ba78f64b4 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -19,6 +19,7 @@ """Code to show a diff of the legacy config format.""" +import typing # pylint: disable=unused-import,useless-suppression import difflib import os.path @@ -727,10 +728,10 @@ scroll right """ -def get_diff(): +def get_diff() -> str: """Get a HTML diff for the old config files.""" - old_conf_lines = [] - old_key_lines = [] + old_conf_lines = [] # type: typing.MutableSequence[str] + old_key_lines = [] # type: typing.MutableSequence[str] for filename, dest in [('qutebrowser.conf', old_conf_lines), ('keys.conf', old_key_lines)]: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index 051ed971a..b1dc04e09 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,9 +19,10 @@ """Exceptions related to config parsing.""" +import typing import attr -from qutebrowser.utils import jinja +from qutebrowser.utils import jinja, usertypes class Error(Exception): @@ -33,7 +34,7 @@ class NoAutoconfigError(Error): """Raised when this option can't be set in autoconfig.yml.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting can only be set in config.py!" .format(name)) @@ -42,7 +43,11 @@ class BackendError(Error): """Raised when this setting is unavailable with the current backend.""" - def __init__(self, name, backend, raw_backends): + def __init__( + self, name: str, + backend: usertypes.Backend, + raw_backends: typing.Optional[typing.Mapping[str, bool]] + ) -> None: if raw_backends is None or not raw_backends[backend.name]: msg = ("The {} setting is not available with the {} backend!" .format(name, backend.name)) @@ -57,7 +62,7 @@ class NoPatternError(Error): """Raised when the given setting does not support URL patterns.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting does not support URL patterns!" .format(name)) @@ -71,7 +76,7 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value, msg): + def __init__(self, value: typing.Any, msg: str) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None @@ -85,7 +90,9 @@ class NoOptionError(Error): """Raised when an option was not found.""" - def __init__(self, option, *, deleted=False, renamed=None): + def __init__(self, option: str, *, + deleted: bool = False, + renamed: str = None) -> None: if deleted: assert renamed is None suffix = ' (this option was removed from qutebrowser)' @@ -109,18 +116,18 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text = attr.ib() - exception = attr.ib() - traceback = attr.ib(None) + text = attr.ib() # type: str + exception = attr.ib() # type: typing.Union[str, Exception] + traceback = attr.ib(None) # type: str - def __str__(self): + def __str__(self) -> str: if self.traceback: return '{} - {}: {}'.format(self.text, self.exception.__class__.__name__, self.exception) return '{}: {}'.format(self.text, self.exception) - def with_text(self, text): + def with_text(self, text: str) -> 'ConfigErrorDesc': """Get a new ConfigErrorDesc with the given text appended.""" return self.__class__(text='{} ({})'.format(self.text, text), exception=self.exception, @@ -131,13 +138,15 @@ class ConfigFileErrors(Error): """Raised when multiple errors occurred inside the config.""" - def __init__(self, basename, errors): + def __init__(self, + basename: str, + errors: typing.Sequence[ConfigErrorDesc]) -> None: super().__init__("Errors occurred while reading {}:\n{}".format( basename, '\n'.join(' {}'.format(e) for e in errors))) self.basename = basename self.errors = errors - def to_html(self): + def to_html(self) -> str: """Get the error texts as a HTML snippet.""" template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index b4c8ea4ec..54ca91488 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -27,6 +27,7 @@ import textwrap import traceback import configparser import contextlib +import typing import yaml from PyQt5.QtCore import pyqtSignal, QObject, QSettings @@ -36,16 +37,21 @@ from qutebrowser.config import configexc, config, configdata, configutils from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch +MYPY = False +if MYPY: + # pylint: disable=unused-import, useless-suppression + from qutebrowser.misc import savemanager + # The StateConfig instance -state = None +state = typing.cast('StateConfig', None) class StateConfig(configparser.ConfigParser): """The "state" file saving various application state.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._filename = os.path.join(standarddir.data(), 'state') self.read(self._filename, encoding='utf-8') @@ -59,7 +65,8 @@ class StateConfig(configparser.ConfigParser): for key in deleted_keys: self['general'].pop(key, None) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -67,7 +74,7 @@ class StateConfig(configparser.ConfigParser): """ save_manager.add_saveable('state-config', self._save) - def _save(self): + def _save(self) -> None: """Save the state file to the configured location.""" with open(self._filename, 'w', encoding='utf-8') as f: self.write(f) @@ -84,17 +91,20 @@ class YamlConfig(QObject): VERSION = 2 changed = pyqtSignal() - def __init__(self, parent=None): + _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), 'autoconfig.yml') - self._dirty = None + self._dirty = False - self._values = {} + self._values = {} # type: typing.Dict[str, configutils.Values] for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -102,21 +112,21 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def _mark_changed(self): + def _mark_changed(self) -> None: """Mark the YAML config as changed.""" self._dirty = True self.changed.emit() - def _save(self): + def _save(self) -> None: """Save the settings to the YAML file if they've changed.""" if not self._dirty: return - settings = {} + settings = {} # type: YamlConfig._SettingsType for name, values in sorted(self._values.items()): if not values: continue @@ -135,7 +145,10 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) - def _pop_object(self, yaml_data, key, typ): + def _pop_object(self, + yaml_data: typing.Any, + key: str, + typ: type) -> typing.Any: """Get a global object from the given data.""" if not isinstance(yaml_data, dict): desc = configexc.ConfigErrorDesc("While loading data", @@ -158,7 +171,7 @@ class YamlConfig(QObject): return data - def load(self): + def load(self) -> None: """Load configuration from the configured YAML file.""" try: with open(self._filename, 'r', encoding='utf-8') as f: @@ -189,18 +202,19 @@ class YamlConfig(QObject): self._validate(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data): + def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType': """Load the settings from the settings: key.""" return self._pop_object(yaml_data, 'settings', dict) - def _load_legacy_settings_object(self, yaml_data): + def _load_legacy_settings_object(self, + yaml_data: typing.Any) -> '_SettingsType': data = self._pop_object(yaml_data, 'global', dict) settings = {} for name, value in data.items(): settings[name] = {'global': value} return settings - def _build_values(self, settings): + def _build_values(self, settings: typing.Mapping) -> None: """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -233,7 +247,8 @@ class YamlConfig(QObject): if errors: raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def _migrate_bool(self, settings, name, true_value, false_value): + def _migrate_bool(self, settings: _SettingsType, name: str, + true_value: str, false_value: str) -> None: """Migrate a boolean in the settings.""" if name in settings: for scope, val in settings[name].items(): @@ -241,7 +256,7 @@ class YamlConfig(QObject): settings[name][scope] = true_value if val else false_value self._mark_changed() - def _handle_migrations(self, settings): + def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType': """Migrate older configs to the newest format.""" # Simple renamed/deleted options for name in list(settings): @@ -299,7 +314,7 @@ class YamlConfig(QObject): return settings - def _validate(self, settings): + def _validate(self, settings: _SettingsType) -> None: """Make sure all settings exist.""" unknown = [] for name in settings: @@ -312,18 +327,19 @@ class YamlConfig(QObject): for e in sorted(unknown)] raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def set_obj(self, name, value, *, pattern=None): + def set_obj(self, name: str, value: typing.Any, *, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting to the given value.""" self._values[name].add(value, pattern) self._mark_changed() - def unset(self, name, *, pattern=None): + def unset(self, name: str, *, pattern: urlmatch.UrlPattern = None) -> None: """Remove the given option name if it's configured.""" changed = self._values[name].remove(pattern) if changed: self._mark_changed() - def clear(self): + def clear(self) -> None: """Clear all values from the YAML file.""" for values in self._values.values(): values.clear() @@ -346,15 +362,15 @@ class ConfigAPI: datadir: The qutebrowser data directory, as pathlib.Path. """ - def __init__(self, conf, keyconfig): + def __init__(self, conf: config.Config, keyconfig: config.KeyConfig): self._config = conf self._keyconfig = keyconfig - self.errors = [] + self.errors = [] # type: typing.List[configexc.ConfigErrorDesc] self.configdir = pathlib.Path(standarddir.config()) self.datadir = pathlib.Path(standarddir.data()) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: """Catch config-related exceptions and save them in self.errors.""" try: yield @@ -372,40 +388,40 @@ class ConfigAPI: text = "While {} '{}' and parsing key".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) - def finalize(self): + def finalize(self) -> None: """Do work which needs to be done after reading config.py.""" self._config.update_mutables() - def load_autoconfig(self): + def load_autoconfig(self) -> None: """Load the autoconfig.yml file which is used for :set/:bind/etc.""" with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name, pattern=None): + def get(self, name: str, pattern: str = None) -> typing.Any: """Get a setting value from the config, optionally with a pattern.""" with self._handle_error('getting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None return self._config.get_mutable_obj(name, pattern=urlpattern) - def set(self, name, value, pattern=None): + def set(self, name: str, value: typing.Any, pattern: str = None) -> None: """Set a setting value in the config, optionally with a pattern.""" with self._handle_error('setting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None self._config.set_obj(name, value, pattern=urlpattern) - def bind(self, key, command, mode='normal'): + def bind(self, key: str, command: str, mode: str = 'normal') -> None: """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.bind(seq, command, mode=mode) - def unbind(self, key, mode='normal'): + def unbind(self, key: str, mode: str = 'normal') -> None: """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.unbind(seq, mode=mode) - def source(self, filename): + def source(self, filename: str) -> None: """Read the given config file from disk.""" if not os.path.isabs(filename): filename = str(self.configdir / filename) @@ -416,7 +432,7 @@ class ConfigAPI: self.errors += e.errors @contextlib.contextmanager - def pattern(self, pattern): + def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]: """Get a ConfigContainer for the given pattern.""" # We need to propagate the exception so we don't need to return # something. @@ -430,17 +446,21 @@ class ConfigPyWriter: """Writer for config.py files from given settings.""" - def __init__(self, options, bindings, *, commented): + def __init__( + self, + options: typing.List, + bindings: typing.MutableMapping[str, typing.Mapping[str, str]], *, + commented: bool) -> None: self._options = options self._bindings = bindings self._commented = commented - def write(self, filename): + def write(self, filename: str) -> None: """Write the config to the given file.""" with open(filename, 'w', encoding='utf-8') as f: f.write('\n'.join(self._gen_lines())) - def _line(self, line): + def _line(self, line: str) -> str: """Get an (optionally commented) line.""" if self._commented: if line.startswith('#'): @@ -450,7 +470,7 @@ class ConfigPyWriter: else: return line - def _gen_lines(self): + def _gen_lines(self) -> typing.Iterator[str]: """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -459,7 +479,7 @@ class ConfigPyWriter: yield from self._gen_options() yield from self._gen_bindings() - def _gen_header(self): + def _gen_header(self) -> typing.Iterator[str]: """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") yield self._line("# Documentation:") @@ -481,7 +501,7 @@ class ConfigPyWriter: yield self._line("# config.load_autoconfig()") yield '' - def _gen_options(self): + def _gen_options(self) -> typing.Iterator[str]: """Generate the options part of the config.""" for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: @@ -509,7 +529,7 @@ class ConfigPyWriter: opt.name, value, str(pattern))) yield '' - def _gen_bindings(self): + def _gen_bindings(self) -> typing.Iterator[str]: """Generate the bindings part of the config.""" normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: @@ -527,7 +547,7 @@ class ConfigPyWriter: yield '' -def read_config_py(filename, raising=False): +def read_config_py(filename: str, raising: bool = False) -> None: """Read a config.py file. Arguments; @@ -543,8 +563,8 @@ def read_config_py(filename, raising=False): basename = os.path.basename(filename) module = types.ModuleType('config') - module.config = api - module.c = container + module.config = api # type: ignore + module.c = container # type: ignore module.__file__ = filename try: @@ -589,7 +609,7 @@ def read_config_py(filename, raising=False): raise configexc.ConfigFileErrors('config.py', api.errors) -def read_autoconfig(): +def read_autoconfig() -> None: """Read the autoconfig.yml file.""" try: config.instance.read_yaml() @@ -601,7 +621,7 @@ def read_autoconfig(): @contextlib.contextmanager -def saved_sys_properties(): +def saved_sys_properties() -> typing.Iterator[None]: """Save various sys properties such as sys.path and sys.modules.""" old_path = sys.path.copy() old_modules = sys.modules.copy() @@ -614,7 +634,7 @@ def saved_sys_properties(): del sys.modules[module] -def init(): +def init() -> None: """Initialize config storage not related to the main config.""" global state state = StateConfig() diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index de9651064..ff0fd0e41 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -19,8 +19,10 @@ """Initialization of the configuration.""" +import argparse import os.path import sys +import typing from PyQt5.QtWidgets import QMessageBox @@ -30,14 +32,14 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, qtutils) from qutebrowser.config import configcache -from qutebrowser.misc import msgbox, objects +from qutebrowser.misc import msgbox, objects, savemanager # Error which happened during init, so we can show a message box. _init_errors = None -def early_init(args): +def early_init(args: argparse.Namespace) -> None: """Initialize the part of the config which works without a QApplication.""" configdata.init() @@ -85,7 +87,7 @@ def early_init(args): _init_envvars() -def _init_envvars(): +def _init_envvars() -> None: """Initialize environment variables which need to be set early.""" if objects.backend == usertypes.Backend.QtWebEngine: software_rendering = config.val.qt.force_software_rendering @@ -107,7 +109,7 @@ def _init_envvars(): @config.change_filter('fonts.monospace', function=True) -def _update_monospace_fonts(): +def _update_monospace_fonts() -> None: """Update all fonts if fonts.monospace was set.""" configtypes.Font.monospace_fonts = config.val.fonts.monospace for name, opt in configdata.DATA.items(): @@ -123,7 +125,7 @@ def _update_monospace_fonts(): config.instance.changed.emit(name) -def get_backend(args): +def get_backend(args: argparse.Namespace) -> usertypes.Backend: """Find out what backend to use based on available libraries.""" str_to_backend = { 'webkit': usertypes.Backend.QtWebKit, @@ -136,7 +138,7 @@ def get_backend(args): return str_to_backend[config.val.backend] -def late_init(save_manager): +def late_init(save_manager: savemanager.SaveManager) -> None: """Initialize the rest of the config after the QApplication is created.""" global _init_errors if _init_errors is not None: @@ -152,7 +154,7 @@ def late_init(save_manager): configfiles.state.init_save_manager(save_manager) -def qt_args(namespace): +def qt_args(namespace: argparse.Namespace) -> typing.List[str]: """Get the Qt QApplication arguments based on an argparse namespace. Args: @@ -178,7 +180,7 @@ def qt_args(namespace): return argv -def _qtwebengine_args(): +def _qtwebengine_args() -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to @@ -224,7 +226,7 @@ def _qtwebengine_args(): 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } - } + } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 96fc0f02d..9d4dc94ef 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -21,11 +21,19 @@ """Utilities and data structures used by various config code.""" -import attr +import typing -from qutebrowser.utils import utils +import attr +from PyQt5.QtCore import QUrl + +from qutebrowser.utils import utils, urlmatch from qutebrowser.config import configexc +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config import configdata + class _UnsetObject: @@ -33,7 +41,7 @@ class _UnsetObject: __slots__ = () - def __repr__(self): + def __repr__(self) -> str: return '' @@ -50,8 +58,8 @@ class ScopedValue: pattern: The UrlPattern for the value, or None for global values. """ - value = attr.ib() - pattern = attr.ib() + value = attr.ib() # type: typing.Any + pattern = attr.ib() # type: typing.Optional[urlmatch.UrlPattern] class Values: @@ -73,15 +81,17 @@ class Values: opt: The Option being customized. """ - def __init__(self, opt, values=None): + def __init__(self, + opt: 'configdata.Option', + values: typing.MutableSequence = None) -> None: self.opt = opt self._values = values or [] - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, opt=self.opt, values=self._values, constructor=True) - def __str__(self): + def __str__(self) -> str: """Get the values as human-readable string.""" if not self: return '{}: '.format(self.opt.name) @@ -96,7 +106,7 @@ class Values: scoped.pattern, self.opt.name, str_value)) return '\n'.join(lines) - def __iter__(self): + def __iter__(self) -> typing.Iterator['ScopedValue']: """Yield ScopedValue elements. This yields in "normal" order, i.e. global and then first-set settings @@ -104,23 +114,25 @@ class Values: """ yield from self._values - def __bool__(self): + def __bool__(self) -> bool: """Check whether this value is customized.""" return bool(self._values) - def _check_pattern_support(self, arg): + def _check_pattern_support( + self, arg: typing.Optional[urlmatch.UrlPattern]) -> None: """Make sure patterns are supported if one was given.""" if arg is not None and not self.opt.supports_pattern: raise configexc.NoPatternError(self.opt.name) - def add(self, value, pattern=None): + def add(self, value: typing.Any, + pattern: urlmatch.UrlPattern = None) -> None: """Add a value with the given pattern to the list of values.""" self._check_pattern_support(pattern) self.remove(pattern) scoped = ScopedValue(value, pattern) self._values.append(scoped) - def remove(self, pattern=None): + def remove(self, pattern: urlmatch.UrlPattern = None) -> bool: """Remove the value with the given pattern. If a matching pattern was removed, True is returned. @@ -131,11 +143,11 @@ class Values: self._values = [v for v in self._values if v.pattern != pattern] return old_len != len(self._values) - def clear(self): + def clear(self) -> None: """Clear all customization for this value.""" self._values = [] - def _get_fallback(self, fallback): + def _get_fallback(self, fallback: typing.Any) -> typing.Any: """Get the fallback global/default value.""" for scoped in self._values: if scoped.pattern is None: @@ -146,7 +158,8 @@ class Values: else: return UNSET - def get_for_url(self, url=None, *, fallback=True): + def get_for_url(self, url: QUrl = None, *, + fallback: bool = True) -> typing.Any: """Get a config value, falling back when needed. This first tries to find a value matching the URL (if given). @@ -165,7 +178,9 @@ class Values: return self._get_fallback(fallback) - def get_for_pattern(self, pattern, *, fallback=True): + def get_for_pattern(self, + pattern: typing.Optional[urlmatch.UrlPattern], *, + fallback: bool = True) -> typing.Any: """Get a value only if it's been overridden for the given pattern. This is useful when showing values to the user. From 7494d238ce6248a217ab958753777ed093d9411e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 13:17:23 +0100 Subject: [PATCH 161/258] Revert "Add types for most of qutebrowser.config" It breaks 'tsh' because of the *values annotation. This reverts commit 208d3db475cf5e5fd50840dd80bfef0fc85e98f5. --- mypy.ini | 36 ------ qutebrowser/config/config.py | 184 ++++++++++----------------- qutebrowser/config/configcommands.py | 97 ++++++-------- qutebrowser/config/configdata.py | 59 +++------ qutebrowser/config/configdiff.py | 7 +- qutebrowser/config/configexc.py | 35 ++--- qutebrowser/config/configfiles.py | 114 +++++++---------- qutebrowser/config/configinit.py | 20 ++- qutebrowser/config/configutils.py | 47 +++---- 9 files changed, 215 insertions(+), 384 deletions(-) diff --git a/mypy.ini b/mypy.ini index be5424327..288cc6515 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,39 +61,3 @@ disallow_incomplete_defs = True [mypy-qutebrowser.commands.cmdutils] disallow_untyped_defs = True disallow_incomplete_defs = True - -[mypy-qutebrowser.config.config] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configcache] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configcommands] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configdata] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configdiff] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configexc] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configfiles] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configinit] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configutils] -disallow_untyped_defs = True -disallow_incomplete_defs = True diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 44bf3ca77..1d7e34345 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,21 +23,18 @@ import copy import contextlib import functools import typing -from typing import Any -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from qutebrowser.config import configdata, configexc, configutils -from qutebrowser.utils import utils, log, jinja, urlmatch +from qutebrowser.utils import utils, log, jinja from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: # pragma: no cover - # pylint: disable=unused-import,useless-suppression - from typing import Tuple, MutableMapping - from qutebrowser.config import configcache, configfiles - from qutebrowser.misc import savemanager +if MYPY: + # pylint: disable=unused-import + from qutebrowser.config import configcache # pragma: no cover # An easy way to access the config from other code via config.val.foo val = typing.cast('ConfigContainer', None) @@ -64,7 +61,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name _function: Whether a function rather than a method is decorated. """ - def __init__(self, option: str, function: bool = False) -> None: + def __init__(self, option, function=False): """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -77,7 +74,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name self._function = function change_filters.append(self) - def validate(self) -> None: + def validate(self): """Make sure the configured option or prefix exists. We can't do this in __init__ as configdata isn't ready yet. @@ -86,7 +83,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): """Check if the given option matches the filter.""" if option is None: # Called directly, not from a config change event. @@ -99,7 +96,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func): """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -117,21 +114,20 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def func_wrapper(option: str = None) -> typing.Any: + def wrapper(option=None): """Call the underlying function.""" if self._check_match(option): return func() return None - return func_wrapper else: @functools.wraps(func) - def meth_wrapper(wrapper_self: typing.Any, - option: str = None) -> typing.Any: + def wrapper(wrapper_self, option=None): """Call the underlying function.""" if self._check_match(option): return func(wrapper_self) return None - return meth_wrapper + + return wrapper class KeyConfig: @@ -144,22 +140,17 @@ class KeyConfig: _config: The Config object to be used. """ - _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]] - - def __init__(self, config: 'Config') -> None: + def __init__(self, config): self._config = config - def _validate(self, key: keyutils.KeySequence, mode: str) -> None: + def _validate(self, key, mode): """Validate the given key and mode.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - def get_bindings_for( - self, - mode: str - ) -> typing.Dict[keyutils.KeySequence, str]: + def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" bindings = dict(val.bindings.default[mode]) for key, binding in val.bindings.commands[mode].items(): @@ -169,9 +160,9 @@ class KeyConfig: bindings[key] = binding return bindings - def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': + def get_reverse_bindings_for(self, mode): """Get a dict of commands to a list of bindings for the mode.""" - cmd_to_keys = {} # type: KeyConfig._ReverseBindings + cmd_to_keys = {} bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): @@ -184,10 +175,7 @@ class KeyConfig: cmd_to_keys[cmd].insert(0, str(seq)) return cmd_to_keys - def get_command(self, - key: keyutils.KeySequence, - mode: str, - default: bool = False) -> str: + def get_command(self, key, mode, default=False): """Get the command for a given key (or None).""" self._validate(key, mode) if default: @@ -196,11 +184,7 @@ class KeyConfig: bindings = self.get_bindings_for(mode) return bindings.get(key, None) - def bind(self, - key: keyutils.KeySequence, - command: str, *, - mode: str, - save_yaml: bool = False) -> None: + def bind(self, key, command, *, mode, save_yaml=False): """Add a new binding from key to command.""" if command is not None and not command.strip(): raise configexc.KeybindingError( @@ -208,8 +192,8 @@ class KeyConfig: 'mode'.format(key, mode)) self._validate(key, mode) - log.keyboard.vdebug( # type: ignore - "Adding binding {} -> {} in mode {}.".format(key, command, mode)) + log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( + key, command, mode)) bindings = self._config.get_mutable_obj('bindings.commands') if mode not in bindings: @@ -217,10 +201,7 @@ class KeyConfig: bindings[mode][str(key)] = command self._config.update_mutables(save_yaml=save_yaml) - def bind_default(self, - key: keyutils.KeySequence, *, - mode: str = 'normal', - save_yaml: bool = False) -> None: + def bind_default(self, key, *, mode='normal', save_yaml=False): """Restore a default keybinding.""" self._validate(key, mode) @@ -232,10 +213,7 @@ class KeyConfig: "Can't find binding '{}' in {} mode".format(key, mode)) self._config.update_mutables(save_yaml=save_yaml) - def unbind(self, - key: keyutils.KeySequence, *, - mode: str = 'normal', - save_yaml: bool = False) -> None: + def unbind(self, key, *, mode='normal', save_yaml=False): """Unbind the given key in the given mode.""" self._validate(key, mode) @@ -276,27 +254,24 @@ class Config(QObject): MUTABLE_TYPES = (dict, list) changed = pyqtSignal(str) - def __init__(self, - yaml_config: 'configfiles.YamlConfig', - parent: QObject = None) -> None: + def __init__(self, yaml_config, parent=None): super().__init__(parent) self.changed.connect(_render_stylesheet.cache_clear) - self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] + self._mutables = {} self._yaml = yaml_config self._init_values() - def _init_values(self) -> None: + def _init_values(self): """Populate the self._values dict.""" - self._values = {} # type: typing.Mapping + self._values = {} for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def __iter__(self) -> typing.Iterator[configutils.Values]: + def __iter__(self): """Iterate over configutils.Values items.""" yield from self._values.values() - def init_save_manager(self, - save_manager: 'savemanager.SaveManager') -> None: + def init_save_manager(self, save_manager): """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -304,10 +279,7 @@ class Config(QObject): """ self._yaml.init_save_manager(save_manager) - def _set_value(self, - opt: 'configdata.Option', - value: Any, - pattern: urlmatch.UrlPattern = None) -> None: + def _set_value(self, opt, value, pattern=None): """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: @@ -322,12 +294,12 @@ class Config(QObject): log.config.debug("Config option changed: {} = {}".format( opt.name, value)) - def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None: + def _check_yaml(self, opt, save_yaml): """Make sure the given option may be set in autoconfig.yml.""" if save_yaml and opt.no_autoconfig: raise configexc.NoAutoconfigError(opt.name) - def read_yaml(self) -> None: + def read_yaml(self): """Read the YAML settings from self._yaml.""" self._yaml.load() for values in self._yaml: @@ -335,7 +307,7 @@ class Config(QObject): self._set_value(values.opt, scoped.value, pattern=scoped.pattern) - def get_opt(self, name: str) -> 'configdata.Option': + def get_opt(self, name): """Get a configdata.Option object for the given setting.""" try: return configdata.DATA[name] @@ -346,10 +318,7 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, - name: str, - url: QUrl = None, *, - fallback: bool = True) -> Any: + def get(self, name, url=None, *, fallback=True): """Get the given setting converted for Python code. Args: @@ -359,7 +328,7 @@ class Config(QObject): obj = self.get_obj(name, url=url, fallback=fallback) return opt.typ.to_py(obj) - def _maybe_copy(self, value: Any) -> Any: + def _maybe_copy(self, value): """Copy the value if it could potentially be mutated.""" if isinstance(value, self.MUTABLE_TYPES): # For mutable objects, create a copy so we don't accidentally @@ -370,10 +339,7 @@ class Config(QObject): assert value.__hash__ is not None, value return value - def get_obj(self, - name: str, *, - url: QUrl = None, - fallback: bool = True) -> Any: + def get_obj(self, name, *, url=None, fallback=True): """Get the given setting as object (for YAML/config.py). Note that the returned values are not watched for mutation. @@ -383,10 +349,7 @@ class Config(QObject): value = self._values[name].get_for_url(url, fallback=fallback) return self._maybe_copy(value) - def get_obj_for_pattern( - self, name: str, *, - pattern: typing.Optional[urlmatch.UrlPattern] - ) -> Any: + def get_obj_for_pattern(self, name, *, pattern): """Get the given setting as object (for YAML/config.py). This gets the overridden value for a given pattern, or @@ -396,8 +359,7 @@ class Config(QObject): value = self._values[name].get_for_pattern(pattern, fallback=False) return self._maybe_copy(value) - def get_mutable_obj(self, name: str, *, - pattern: urlmatch.UrlPattern = None) -> Any: + def get_mutable_obj(self, name, *, pattern=None): """Get an object which can be mutated, e.g. in a config.py. If a pattern is given, return the value for that pattern. @@ -422,8 +384,7 @@ class Config(QObject): return copy_value - def get_str(self, name: str, *, - pattern: urlmatch.UrlPattern = None) -> str: + def get_str(self, name, *, pattern=None): """Get the given setting as string. If a pattern is given, get the setting for the given pattern or @@ -434,10 +395,7 @@ class Config(QObject): value = values.get_for_pattern(pattern) return opt.typ.to_str(value) - def set_obj(self, name: str, - value: Any, *, - pattern: urlmatch.UrlPattern = None, - save_yaml: bool = False) -> None: + def set_obj(self, name, value, *, pattern=None, save_yaml=False): """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. @@ -448,10 +406,7 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, value, pattern=pattern) - def set_str(self, name: str, - value: str, *, - pattern: urlmatch.UrlPattern = None, - save_yaml: bool = False) -> None: + def set_str(self, name, value, *, pattern=None, save_yaml=False): """Set the given setting from a string. If save_yaml=True is given, store the new value to YAML. @@ -466,9 +421,7 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, converted, pattern=pattern) - def unset(self, name: str, *, - save_yaml: bool = False, - pattern: urlmatch.UrlPattern = None) -> None: + def unset(self, name, *, save_yaml=False, pattern=None): """Set the given setting back to its default.""" opt = self.get_opt(name) self._check_yaml(opt, save_yaml) @@ -479,7 +432,7 @@ class Config(QObject): if save_yaml: self._yaml.unset(name, pattern=pattern) - def clear(self, *, save_yaml: bool = False) -> None: + def clear(self, *, save_yaml=False): """Clear all settings in the config. If save_yaml=True is given, also remove all customization from the YAML @@ -493,7 +446,7 @@ class Config(QObject): if save_yaml: self._yaml.clear() - def update_mutables(self, *, save_yaml: bool = False) -> None: + def update_mutables(self, *, save_yaml=False): """Update mutable settings if they changed. Every time someone calls get_obj() on a mutable object, we save a @@ -508,7 +461,7 @@ class Config(QObject): self.set_obj(name, new_value, save_yaml=save_yaml) self._mutables = {} - def dump_userconfig(self) -> str: + def dump_userconfig(self): """Get the part of the config which was changed by the user. Return: @@ -537,10 +490,7 @@ class ConfigContainer: _pattern: The URL pattern to be used. """ - def __init__(self, config: Config, - configapi: 'configfiles.ConfigAPI' = None, - prefix: str = '', - pattern: urlmatch.UrlPattern = None) -> None: + def __init__(self, config, configapi=None, prefix='', pattern=None): self._config = config self._prefix = prefix self._configapi = configapi @@ -548,13 +498,13 @@ class ConfigContainer: if configapi is None and pattern is not None: raise TypeError("Can't use pattern without configapi!") - def __repr__(self) -> str: + def __repr__(self): return utils.get_repr(self, constructor=True, config=self._config, configapi=self._configapi, prefix=self._prefix, pattern=self._pattern) @contextlib.contextmanager - def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: + def _handle_error(self, action, name): try: yield except configexc.Error as e: @@ -563,7 +513,7 @@ class ConfigContainer: text = "While {} '{}'".format(action, name) self._configapi.errors.append(configexc.ConfigErrorDesc(text, e)) - def __getattr__(self, attr: str) -> Any: + def __getattr__(self, attr): """Get an option or a new ConfigContainer with the added prefix. If we get an option which exists, we return the value for it. @@ -590,7 +540,7 @@ class ConfigContainer: return self._config.get_mutable_obj( name, pattern=self._pattern) - def __setattr__(self, attr: str, value: Any) -> None: + def __setattr__(self, attr, value): """Set the given option in the config.""" if attr.startswith('_'): super().__setattr__(attr, value) @@ -600,7 +550,7 @@ class ConfigContainer: with self._handle_error('setting', name): self._config.set_obj(name, value, pattern=self._pattern) - def _join(self, attr: str) -> str: + def _join(self, attr): """Get the prefix joined with the given attribute.""" if self._prefix: return '{}.{}'.format(self._prefix, attr) @@ -608,10 +558,8 @@ class ConfigContainer: return attr -def set_register_stylesheet(obj: QObject, *, - stylesheet: str = None, - update: bool = True) -> None: - """Set the stylesheet for an object. +def set_register_stylesheet(obj, *, stylesheet=None, update=True): + """Set the stylesheet for an object based on it's STYLESHEET attribute. Also, register an update when the config is changed. @@ -626,7 +574,7 @@ def set_register_stylesheet(obj: QObject, *, @functools.lru_cache() -def _render_stylesheet(stylesheet: str) -> str: +def _render_stylesheet(stylesheet): """Render the given stylesheet jinja template.""" with jinja.environment.no_autoescape(): template = jinja.environment.from_string(stylesheet) @@ -642,9 +590,7 @@ class StyleSheetObserver(QObject): _stylesheet: The stylesheet template to use. """ - def __init__(self, obj: QObject, - stylesheet: typing.Optional[str], - update: bool) -> None: + def __init__(self, obj, stylesheet, update): super().__init__() self._obj = obj self._update = update @@ -653,11 +599,11 @@ class StyleSheetObserver(QObject): if self._update: self.setParent(self._obj) if stylesheet is None: - self._stylesheet = obj.STYLESHEET # type: str + self._stylesheet = obj.STYLESHEET else: self._stylesheet = stylesheet - def _get_stylesheet(self) -> str: + def _get_stylesheet(self): """Format a stylesheet based on a template. Return: @@ -666,15 +612,19 @@ class StyleSheetObserver(QObject): return _render_stylesheet(self._stylesheet) @pyqtSlot() - def _update_stylesheet(self) -> None: + def _update_stylesheet(self): """Update the stylesheet for obj.""" self._obj.setStyleSheet(self._get_stylesheet()) - def register(self) -> None: - """Do a first update and listen for more.""" + def register(self): + """Do a first update and listen for more. + + Args: + update: if False, don't listen for future updates. + """ qss = self._get_stylesheet() - log.config.vdebug( # type: ignore - "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) + log.config.vdebug("stylesheet for {}: {}".format( + self._obj.__class__.__name__, qss)) self._obj.setStyleSheet(qss) if self._update: instance.changed.connect(self._update_stylesheet) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 0ee77fcc9..574bc06af 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -19,7 +19,6 @@ """Commands related to the configuration.""" -import typing import os.path import contextlib @@ -32,34 +31,24 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils -MYPY = False -if MYPY: # pragma: no cover - # pylint: disable=unused-import,useless-suppression - from qutebrowser.config.config import Config, KeyConfig - class ConfigCommands: """qutebrowser commands related to the configuration.""" - def __init__(self, - config: 'Config', - keyconfig: 'KeyConfig') -> None: + def __init__(self, config, keyconfig): self._config = config self._keyconfig = keyconfig @contextlib.contextmanager - def _handle_config_error(self) -> typing.Iterator[None]: + def _handle_config_error(self): """Catch errors in set_command and raise CommandError.""" try: yield except configexc.Error as e: raise cmdutils.CommandError(str(e)) - def _parse_pattern( - self, - pattern: typing.Optional[str] - ) -> typing.Optional[urlmatch.UrlPattern]: + def _parse_pattern(self, pattern): """Parse a pattern string argument to a pattern.""" if pattern is None: return None @@ -70,15 +59,14 @@ class ConfigCommands: raise cmdutils.CommandError("Error while parsing {}: {}" .format(pattern, str(e))) - def _parse_key(self, key: str) -> keyutils.KeySequence: + def _parse_key(self, key): """Parse a key argument.""" try: return keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: raise cmdutils.CommandError(str(e)) - def _print_value(self, option: str, - pattern: typing.Optional[urlmatch.UrlPattern]) -> None: + def _print_value(self, option, pattern): """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) @@ -93,9 +81,8 @@ class ConfigCommands: @cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('pattern', flag='u') - def set(self, win_id: int, option: str = None, value: str = None, - temp: bool = False, print_: bool = False, - *, pattern: str = None) -> None: + def set(self, win_id, option=None, value=None, temp=False, print_=False, + *, pattern=None): """Set an option. If the option name ends with '?' or no value is provided, the @@ -121,28 +108,28 @@ class ConfigCommands: raise cmdutils.CommandError("Toggling values was moved to the " ":config-cycle command") - parsed_pattern = self._parse_pattern(pattern) + pattern = self._parse_pattern(pattern) if option.endswith('?') and option != '?': - self._print_value(option[:-1], pattern=parsed_pattern) + self._print_value(option[:-1], pattern=pattern) return with self._handle_config_error(): if value is None: - self._print_value(option, pattern=parsed_pattern) + self._print_value(option, pattern=pattern) else: - self._config.set_str(option, value, pattern=parsed_pattern, + self._config.set_str(option, value, pattern=pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=parsed_pattern) + self._print_value(option, pattern=pattern) @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) - def bind(self, win_id: str, key: str = None, command: str = None, *, - mode: str = 'normal', default: bool = False) -> None: + def bind(self, win_id, key=None, command=None, *, mode='normal', + default=False): """Bind a key to a command. If no command is given, show the current binding for the given key. @@ -187,7 +174,7 @@ class ConfigCommands: self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands') - def unbind(self, key: str, *, mode: str = 'normal') -> None: + def unbind(self, key, *, mode='normal'): """Unbind a keychain. Args: @@ -204,9 +191,8 @@ class ConfigCommands: @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('pattern', flag='u') - def config_cycle(self, option: str, *values: str, - pattern: str = None, - temp: bool = False, print_: bool = False) -> None: + def config_cycle(self, option, *values, pattern=None, temp=False, + print_=False): """Cycle an option between multiple values. Args: @@ -216,15 +202,15 @@ class ConfigCommands: temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ - parsed_pattern = self._parse_pattern(pattern) + pattern = self._parse_pattern(pattern) with self._handle_config_error(): opt = self._config.get_opt(option) - old_value = self._config.get_obj_for_pattern( - option, pattern=parsed_pattern) + old_value = self._config.get_obj_for_pattern(option, + pattern=pattern) if not values and isinstance(opt.typ, configtypes.Bool): - values = ('true', 'false') + values = ['true', 'false'] if len(values) < 2: raise cmdutils.CommandError("Need at least two values for " @@ -233,25 +219,25 @@ class ConfigCommands: # Use the next valid value from values, or the first if the current # value does not appear in the list with self._handle_config_error(): - cycle_values = [opt.typ.from_str(val) for val in values] + values = [opt.typ.from_str(val) for val in values] try: - idx = cycle_values.index(old_value) - idx = (idx + 1) % len(cycle_values) - value = cycle_values[idx] + idx = values.index(old_value) + idx = (idx + 1) % len(values) + value = values[idx] except ValueError: - value = cycle_values[0] + value = values[0] with self._handle_config_error(): - self._config.set_obj(option, value, pattern=parsed_pattern, + self._config.set_obj(option, value, pattern=pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=parsed_pattern) + self._print_value(option, pattern=pattern) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.customized_option) - def config_unset(self, option: str, temp: bool = False) -> None: + def config_unset(self, option, temp=False): """Unset an option. This sets an option back to its default and removes it from @@ -266,8 +252,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_add(self, option: str, value: str, - temp: bool = False) -> None: + def config_list_add(self, option, value, temp=False): """Append a value to a config option that is a list. Args: @@ -288,8 +273,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_add(self, option: str, key: str, value: str, - temp: bool = False, replace: bool = False) -> None: + def config_dict_add(self, option, key, value, temp=False, replace=False): """Add a key/value pair to a dictionary option. Args: @@ -318,8 +302,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_remove(self, option: str, value: str, - temp: bool = False) -> None: + def config_list_remove(self, option, value, temp=False): """Remove a value from a list. Args: @@ -346,8 +329,7 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_remove(self, option: str, key: str, - temp: bool = False) -> None: + def config_dict_remove(self, option, key, temp=False): """Remove a key from a dict. Args: @@ -372,7 +354,7 @@ class ConfigCommands: self._config.update_mutables(save_yaml=not temp) @cmdutils.register(instance='config-commands') - def config_clear(self, save: bool = False) -> None: + def config_clear(self, save=False): """Set all settings back to their default. Args: @@ -382,7 +364,7 @@ class ConfigCommands: self._config.clear(save_yaml=save) @cmdutils.register(instance='config-commands') - def config_source(self, filename: str = None, clear: bool = False) -> None: + def config_source(self, filename=None, clear=False): """Read a config.py file. Args: @@ -404,13 +386,13 @@ class ConfigCommands: raise cmdutils.CommandError(e) @cmdutils.register(instance='config-commands') - def config_edit(self, no_source: bool = False) -> None: + def config_edit(self, no_source=False): """Open the config.py file in the editor. Args: no_source: Don't re-source the config file after editing. """ - def on_file_updated() -> None: + def on_file_updated(): """Source the new config when editing finished. This can't use cmdutils.CommandError as it's run async. @@ -428,8 +410,7 @@ class ConfigCommands: ed.edit_file(filename) @cmdutils.register(instance='config-commands') - def config_write_py(self, filename: str = None, - force: bool = False, defaults: bool = False) -> None: + def config_write_py(self, filename=None, force=False, defaults=False): """Write the current configuration to a config.py file. Args: @@ -448,13 +429,13 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) - options = [] # type: typing.List if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] bindings = dict(configdata.DATA['bindings.default'].default) commented = True else: + options = [] for values in self._config: for scoped in values: options.append((scoped.pattern, values.opt, scoped.value)) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index c93032387..dace0772a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -24,18 +24,14 @@ Module attributes: DATA: A dict of Option objects after init() has been called. """ -import typing -from typing import Optional # pylint: disable=unused-import import functools import attr from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils -DATA = typing.cast(typing.Mapping[str, 'Option'], None) -MIGRATIONS = typing.cast('Migrations', None) - -_BackendDict = typing.Mapping[str, typing.Union[str, bool]] +DATA = None +MIGRATIONS = None @attr.s @@ -46,15 +42,15 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name = attr.ib() # type: str - typ = attr.ib() # type: configtypes.BaseType - default = attr.ib() # type: typing.Any - backends = attr.ib() # type: typing.Iterable[usertypes.Backend] - raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]] - description = attr.ib() # type: str - supports_pattern = attr.ib(default=False) # type: bool - restart = attr.ib(default=False) # type: bool - no_autoconfig = attr.ib(default=False) # type: bool + name = attr.ib() + typ = attr.ib() + default = attr.ib() + backends = attr.ib() + raw_backends = attr.ib() + description = attr.ib() + supports_pattern = attr.ib(default=False) + restart = attr.ib(default=False) + no_autoconfig = attr.ib(default=False) @attr.s @@ -67,13 +63,11 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed = attr.ib( - default=attr.Factory(dict)) # type: typing.Dict[str, str] - deleted = attr.ib( - default=attr.Factory(list)) # type: typing.List[str] + renamed = attr.ib(default=attr.Factory(dict)) + deleted = attr.ib(default=attr.Factory(list)) -def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: +def _raise_invalid_node(name, what, node): """Raise an exception for an invalid configdata YAML node. Args: @@ -85,16 +79,13 @@ def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: name, what, node)) -def _parse_yaml_type( - name: str, - node: typing.Union[str, typing.Mapping[str, typing.Any]], -) -> configtypes.BaseType: +def _parse_yaml_type(name, node): if isinstance(node, str): # e.g: # type: Bool # -> create the type object without any arguments type_name = node - kwargs = {} # type: typing.MutableMapping[str, typing.Any] + kwargs = {} elif isinstance(node, dict): # e.g: # type: @@ -132,10 +123,7 @@ def _parse_yaml_type( type_name, node, e)) -def _parse_yaml_backends_dict( - name: str, - node: _BackendDict, -) -> typing.Sequence[usertypes.Backend]: +def _parse_yaml_backends_dict(name, node): """Parse a dict definition for backends. Example: @@ -172,10 +160,7 @@ def _parse_yaml_backends_dict( return backends -def _parse_yaml_backends( - name: str, - node: typing.Union[None, str, _BackendDict], -) -> typing.Sequence[usertypes.Backend]: +def _parse_yaml_backends(name, node): """Parse a backend node in the yaml. It can have one of those four forms: @@ -202,9 +187,7 @@ def _parse_yaml_backends( raise utils.Unreachable -def _read_yaml( - yaml_data: str, -) -> typing.Tuple[typing.Mapping[str, Option], Migrations]: +def _read_yaml(yaml_data): """Read config data from a YAML file. Args: @@ -266,12 +249,12 @@ def _read_yaml( @functools.lru_cache(maxsize=256) -def is_valid_prefix(prefix: str) -> bool: +def is_valid_prefix(prefix): """Check whether the given prefix is a valid prefix for some option.""" return any(key.startswith(prefix + '.') for key in DATA) -def init() -> None: +def init(): """Initialize configdata from the YAML file.""" global DATA, MIGRATIONS DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml')) diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index ba78f64b4..9f8b70a26 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -19,7 +19,6 @@ """Code to show a diff of the legacy config format.""" -import typing # pylint: disable=unused-import,useless-suppression import difflib import os.path @@ -728,10 +727,10 @@ scroll right """ -def get_diff() -> str: +def get_diff(): """Get a HTML diff for the old config files.""" - old_conf_lines = [] # type: typing.MutableSequence[str] - old_key_lines = [] # type: typing.MutableSequence[str] + old_conf_lines = [] + old_key_lines = [] for filename, dest in [('qutebrowser.conf', old_conf_lines), ('keys.conf', old_key_lines)]: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index b1dc04e09..051ed971a 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,10 +19,9 @@ """Exceptions related to config parsing.""" -import typing import attr -from qutebrowser.utils import jinja, usertypes +from qutebrowser.utils import jinja class Error(Exception): @@ -34,7 +33,7 @@ class NoAutoconfigError(Error): """Raised when this option can't be set in autoconfig.yml.""" - def __init__(self, name: str) -> None: + def __init__(self, name): super().__init__("The {} setting can only be set in config.py!" .format(name)) @@ -43,11 +42,7 @@ class BackendError(Error): """Raised when this setting is unavailable with the current backend.""" - def __init__( - self, name: str, - backend: usertypes.Backend, - raw_backends: typing.Optional[typing.Mapping[str, bool]] - ) -> None: + def __init__(self, name, backend, raw_backends): if raw_backends is None or not raw_backends[backend.name]: msg = ("The {} setting is not available with the {} backend!" .format(name, backend.name)) @@ -62,7 +57,7 @@ class NoPatternError(Error): """Raised when the given setting does not support URL patterns.""" - def __init__(self, name: str) -> None: + def __init__(self, name): super().__init__("The {} setting does not support URL patterns!" .format(name)) @@ -76,7 +71,7 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value: typing.Any, msg: str) -> None: + def __init__(self, value, msg): super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None @@ -90,9 +85,7 @@ class NoOptionError(Error): """Raised when an option was not found.""" - def __init__(self, option: str, *, - deleted: bool = False, - renamed: str = None) -> None: + def __init__(self, option, *, deleted=False, renamed=None): if deleted: assert renamed is None suffix = ' (this option was removed from qutebrowser)' @@ -116,18 +109,18 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text = attr.ib() # type: str - exception = attr.ib() # type: typing.Union[str, Exception] - traceback = attr.ib(None) # type: str + text = attr.ib() + exception = attr.ib() + traceback = attr.ib(None) - def __str__(self) -> str: + def __str__(self): if self.traceback: return '{} - {}: {}'.format(self.text, self.exception.__class__.__name__, self.exception) return '{}: {}'.format(self.text, self.exception) - def with_text(self, text: str) -> 'ConfigErrorDesc': + def with_text(self, text): """Get a new ConfigErrorDesc with the given text appended.""" return self.__class__(text='{} ({})'.format(self.text, text), exception=self.exception, @@ -138,15 +131,13 @@ class ConfigFileErrors(Error): """Raised when multiple errors occurred inside the config.""" - def __init__(self, - basename: str, - errors: typing.Sequence[ConfigErrorDesc]) -> None: + def __init__(self, basename, errors): super().__init__("Errors occurred while reading {}:\n{}".format( basename, '\n'.join(' {}'.format(e) for e in errors))) self.basename = basename self.errors = errors - def to_html(self) -> str: + def to_html(self): """Get the error texts as a HTML snippet.""" template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 54ca91488..b4c8ea4ec 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -27,7 +27,6 @@ import textwrap import traceback import configparser import contextlib -import typing import yaml from PyQt5.QtCore import pyqtSignal, QObject, QSettings @@ -37,21 +36,16 @@ from qutebrowser.config import configexc, config, configdata, configutils from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch -MYPY = False -if MYPY: - # pylint: disable=unused-import, useless-suppression - from qutebrowser.misc import savemanager - # The StateConfig instance -state = typing.cast('StateConfig', None) +state = None class StateConfig(configparser.ConfigParser): """The "state" file saving various application state.""" - def __init__(self) -> None: + def __init__(self): super().__init__() self._filename = os.path.join(standarddir.data(), 'state') self.read(self._filename, encoding='utf-8') @@ -65,8 +59,7 @@ class StateConfig(configparser.ConfigParser): for key in deleted_keys: self['general'].pop(key, None) - def init_save_manager(self, - save_manager: 'savemanager.SaveManager') -> None: + def init_save_manager(self, save_manager): """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -74,7 +67,7 @@ class StateConfig(configparser.ConfigParser): """ save_manager.add_saveable('state-config', self._save) - def _save(self) -> None: + def _save(self): """Save the state file to the configured location.""" with open(self._filename, 'w', encoding='utf-8') as f: self.write(f) @@ -91,20 +84,17 @@ class YamlConfig(QObject): VERSION = 2 changed = pyqtSignal() - _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] - - def __init__(self, parent: QObject = None) -> None: + def __init__(self, parent=None): super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), 'autoconfig.yml') - self._dirty = False + self._dirty = None - self._values = {} # type: typing.Dict[str, configutils.Values] + self._values = {} for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def init_save_manager(self, - save_manager: 'savemanager.SaveManager') -> None: + def init_save_manager(self, save_manager): """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -112,21 +102,21 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __iter__(self) -> typing.Iterator[configutils.Values]: + def __iter__(self): """Iterate over configutils.Values items.""" yield from self._values.values() - def _mark_changed(self) -> None: + def _mark_changed(self): """Mark the YAML config as changed.""" self._dirty = True self.changed.emit() - def _save(self) -> None: + def _save(self): """Save the settings to the YAML file if they've changed.""" if not self._dirty: return - settings = {} # type: YamlConfig._SettingsType + settings = {} for name, values in sorted(self._values.items()): if not values: continue @@ -145,10 +135,7 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) - def _pop_object(self, - yaml_data: typing.Any, - key: str, - typ: type) -> typing.Any: + def _pop_object(self, yaml_data, key, typ): """Get a global object from the given data.""" if not isinstance(yaml_data, dict): desc = configexc.ConfigErrorDesc("While loading data", @@ -171,7 +158,7 @@ class YamlConfig(QObject): return data - def load(self) -> None: + def load(self): """Load configuration from the configured YAML file.""" try: with open(self._filename, 'r', encoding='utf-8') as f: @@ -202,19 +189,18 @@ class YamlConfig(QObject): self._validate(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType': + def _load_settings_object(self, yaml_data): """Load the settings from the settings: key.""" return self._pop_object(yaml_data, 'settings', dict) - def _load_legacy_settings_object(self, - yaml_data: typing.Any) -> '_SettingsType': + def _load_legacy_settings_object(self, yaml_data): data = self._pop_object(yaml_data, 'global', dict) settings = {} for name, value in data.items(): settings[name] = {'global': value} return settings - def _build_values(self, settings: typing.Mapping) -> None: + def _build_values(self, settings): """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -247,8 +233,7 @@ class YamlConfig(QObject): if errors: raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def _migrate_bool(self, settings: _SettingsType, name: str, - true_value: str, false_value: str) -> None: + def _migrate_bool(self, settings, name, true_value, false_value): """Migrate a boolean in the settings.""" if name in settings: for scope, val in settings[name].items(): @@ -256,7 +241,7 @@ class YamlConfig(QObject): settings[name][scope] = true_value if val else false_value self._mark_changed() - def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType': + def _handle_migrations(self, settings): """Migrate older configs to the newest format.""" # Simple renamed/deleted options for name in list(settings): @@ -314,7 +299,7 @@ class YamlConfig(QObject): return settings - def _validate(self, settings: _SettingsType) -> None: + def _validate(self, settings): """Make sure all settings exist.""" unknown = [] for name in settings: @@ -327,19 +312,18 @@ class YamlConfig(QObject): for e in sorted(unknown)] raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def set_obj(self, name: str, value: typing.Any, *, - pattern: urlmatch.UrlPattern = None) -> None: + def set_obj(self, name, value, *, pattern=None): """Set the given setting to the given value.""" self._values[name].add(value, pattern) self._mark_changed() - def unset(self, name: str, *, pattern: urlmatch.UrlPattern = None) -> None: + def unset(self, name, *, pattern=None): """Remove the given option name if it's configured.""" changed = self._values[name].remove(pattern) if changed: self._mark_changed() - def clear(self) -> None: + def clear(self): """Clear all values from the YAML file.""" for values in self._values.values(): values.clear() @@ -362,15 +346,15 @@ class ConfigAPI: datadir: The qutebrowser data directory, as pathlib.Path. """ - def __init__(self, conf: config.Config, keyconfig: config.KeyConfig): + def __init__(self, conf, keyconfig): self._config = conf self._keyconfig = keyconfig - self.errors = [] # type: typing.List[configexc.ConfigErrorDesc] + self.errors = [] self.configdir = pathlib.Path(standarddir.config()) self.datadir = pathlib.Path(standarddir.data()) @contextlib.contextmanager - def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: + def _handle_error(self, action, name): """Catch config-related exceptions and save them in self.errors.""" try: yield @@ -388,40 +372,40 @@ class ConfigAPI: text = "While {} '{}' and parsing key".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) - def finalize(self) -> None: + def finalize(self): """Do work which needs to be done after reading config.py.""" self._config.update_mutables() - def load_autoconfig(self) -> None: + def load_autoconfig(self): """Load the autoconfig.yml file which is used for :set/:bind/etc.""" with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name: str, pattern: str = None) -> typing.Any: + def get(self, name, pattern=None): """Get a setting value from the config, optionally with a pattern.""" with self._handle_error('getting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None return self._config.get_mutable_obj(name, pattern=urlpattern) - def set(self, name: str, value: typing.Any, pattern: str = None) -> None: + def set(self, name, value, pattern=None): """Set a setting value in the config, optionally with a pattern.""" with self._handle_error('setting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None self._config.set_obj(name, value, pattern=urlpattern) - def bind(self, key: str, command: str, mode: str = 'normal') -> None: + def bind(self, key, command, mode='normal'): """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.bind(seq, command, mode=mode) - def unbind(self, key: str, mode: str = 'normal') -> None: + def unbind(self, key, mode='normal'): """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.unbind(seq, mode=mode) - def source(self, filename: str) -> None: + def source(self, filename): """Read the given config file from disk.""" if not os.path.isabs(filename): filename = str(self.configdir / filename) @@ -432,7 +416,7 @@ class ConfigAPI: self.errors += e.errors @contextlib.contextmanager - def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]: + def pattern(self, pattern): """Get a ConfigContainer for the given pattern.""" # We need to propagate the exception so we don't need to return # something. @@ -446,21 +430,17 @@ class ConfigPyWriter: """Writer for config.py files from given settings.""" - def __init__( - self, - options: typing.List, - bindings: typing.MutableMapping[str, typing.Mapping[str, str]], *, - commented: bool) -> None: + def __init__(self, options, bindings, *, commented): self._options = options self._bindings = bindings self._commented = commented - def write(self, filename: str) -> None: + def write(self, filename): """Write the config to the given file.""" with open(filename, 'w', encoding='utf-8') as f: f.write('\n'.join(self._gen_lines())) - def _line(self, line: str) -> str: + def _line(self, line): """Get an (optionally commented) line.""" if self._commented: if line.startswith('#'): @@ -470,7 +450,7 @@ class ConfigPyWriter: else: return line - def _gen_lines(self) -> typing.Iterator[str]: + def _gen_lines(self): """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -479,7 +459,7 @@ class ConfigPyWriter: yield from self._gen_options() yield from self._gen_bindings() - def _gen_header(self) -> typing.Iterator[str]: + def _gen_header(self): """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") yield self._line("# Documentation:") @@ -501,7 +481,7 @@ class ConfigPyWriter: yield self._line("# config.load_autoconfig()") yield '' - def _gen_options(self) -> typing.Iterator[str]: + def _gen_options(self): """Generate the options part of the config.""" for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: @@ -529,7 +509,7 @@ class ConfigPyWriter: opt.name, value, str(pattern))) yield '' - def _gen_bindings(self) -> typing.Iterator[str]: + def _gen_bindings(self): """Generate the bindings part of the config.""" normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: @@ -547,7 +527,7 @@ class ConfigPyWriter: yield '' -def read_config_py(filename: str, raising: bool = False) -> None: +def read_config_py(filename, raising=False): """Read a config.py file. Arguments; @@ -563,8 +543,8 @@ def read_config_py(filename: str, raising: bool = False) -> None: basename = os.path.basename(filename) module = types.ModuleType('config') - module.config = api # type: ignore - module.c = container # type: ignore + module.config = api + module.c = container module.__file__ = filename try: @@ -609,7 +589,7 @@ def read_config_py(filename: str, raising: bool = False) -> None: raise configexc.ConfigFileErrors('config.py', api.errors) -def read_autoconfig() -> None: +def read_autoconfig(): """Read the autoconfig.yml file.""" try: config.instance.read_yaml() @@ -621,7 +601,7 @@ def read_autoconfig() -> None: @contextlib.contextmanager -def saved_sys_properties() -> typing.Iterator[None]: +def saved_sys_properties(): """Save various sys properties such as sys.path and sys.modules.""" old_path = sys.path.copy() old_modules = sys.modules.copy() @@ -634,7 +614,7 @@ def saved_sys_properties() -> typing.Iterator[None]: del sys.modules[module] -def init() -> None: +def init(): """Initialize config storage not related to the main config.""" global state state = StateConfig() diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index ff0fd0e41..de9651064 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -19,10 +19,8 @@ """Initialization of the configuration.""" -import argparse import os.path import sys -import typing from PyQt5.QtWidgets import QMessageBox @@ -32,14 +30,14 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, qtutils) from qutebrowser.config import configcache -from qutebrowser.misc import msgbox, objects, savemanager +from qutebrowser.misc import msgbox, objects # Error which happened during init, so we can show a message box. _init_errors = None -def early_init(args: argparse.Namespace) -> None: +def early_init(args): """Initialize the part of the config which works without a QApplication.""" configdata.init() @@ -87,7 +85,7 @@ def early_init(args: argparse.Namespace) -> None: _init_envvars() -def _init_envvars() -> None: +def _init_envvars(): """Initialize environment variables which need to be set early.""" if objects.backend == usertypes.Backend.QtWebEngine: software_rendering = config.val.qt.force_software_rendering @@ -109,7 +107,7 @@ def _init_envvars() -> None: @config.change_filter('fonts.monospace', function=True) -def _update_monospace_fonts() -> None: +def _update_monospace_fonts(): """Update all fonts if fonts.monospace was set.""" configtypes.Font.monospace_fonts = config.val.fonts.monospace for name, opt in configdata.DATA.items(): @@ -125,7 +123,7 @@ def _update_monospace_fonts() -> None: config.instance.changed.emit(name) -def get_backend(args: argparse.Namespace) -> usertypes.Backend: +def get_backend(args): """Find out what backend to use based on available libraries.""" str_to_backend = { 'webkit': usertypes.Backend.QtWebKit, @@ -138,7 +136,7 @@ def get_backend(args: argparse.Namespace) -> usertypes.Backend: return str_to_backend[config.val.backend] -def late_init(save_manager: savemanager.SaveManager) -> None: +def late_init(save_manager): """Initialize the rest of the config after the QApplication is created.""" global _init_errors if _init_errors is not None: @@ -154,7 +152,7 @@ def late_init(save_manager: savemanager.SaveManager) -> None: configfiles.state.init_save_manager(save_manager) -def qt_args(namespace: argparse.Namespace) -> typing.List[str]: +def qt_args(namespace): """Get the Qt QApplication arguments based on an argparse namespace. Args: @@ -180,7 +178,7 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]: return argv -def _qtwebengine_args() -> typing.Iterator[str]: +def _qtwebengine_args(): """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to @@ -226,7 +224,7 @@ def _qtwebengine_args() -> typing.Iterator[str]: 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } - } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] + } if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 9d4dc94ef..96fc0f02d 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -21,19 +21,11 @@ """Utilities and data structures used by various config code.""" -import typing - import attr -from PyQt5.QtCore import QUrl -from qutebrowser.utils import utils, urlmatch +from qutebrowser.utils import utils from qutebrowser.config import configexc -MYPY = False -if MYPY: # pragma: no cover - # pylint: disable=unused-import,useless-suppression - from qutebrowser.config import configdata - class _UnsetObject: @@ -41,7 +33,7 @@ class _UnsetObject: __slots__ = () - def __repr__(self) -> str: + def __repr__(self): return '' @@ -58,8 +50,8 @@ class ScopedValue: pattern: The UrlPattern for the value, or None for global values. """ - value = attr.ib() # type: typing.Any - pattern = attr.ib() # type: typing.Optional[urlmatch.UrlPattern] + value = attr.ib() + pattern = attr.ib() class Values: @@ -81,17 +73,15 @@ class Values: opt: The Option being customized. """ - def __init__(self, - opt: 'configdata.Option', - values: typing.MutableSequence = None) -> None: + def __init__(self, opt, values=None): self.opt = opt self._values = values or [] - def __repr__(self) -> str: + def __repr__(self): return utils.get_repr(self, opt=self.opt, values=self._values, constructor=True) - def __str__(self) -> str: + def __str__(self): """Get the values as human-readable string.""" if not self: return '{}: '.format(self.opt.name) @@ -106,7 +96,7 @@ class Values: scoped.pattern, self.opt.name, str_value)) return '\n'.join(lines) - def __iter__(self) -> typing.Iterator['ScopedValue']: + def __iter__(self): """Yield ScopedValue elements. This yields in "normal" order, i.e. global and then first-set settings @@ -114,25 +104,23 @@ class Values: """ yield from self._values - def __bool__(self) -> bool: + def __bool__(self): """Check whether this value is customized.""" return bool(self._values) - def _check_pattern_support( - self, arg: typing.Optional[urlmatch.UrlPattern]) -> None: + def _check_pattern_support(self, arg): """Make sure patterns are supported if one was given.""" if arg is not None and not self.opt.supports_pattern: raise configexc.NoPatternError(self.opt.name) - def add(self, value: typing.Any, - pattern: urlmatch.UrlPattern = None) -> None: + def add(self, value, pattern=None): """Add a value with the given pattern to the list of values.""" self._check_pattern_support(pattern) self.remove(pattern) scoped = ScopedValue(value, pattern) self._values.append(scoped) - def remove(self, pattern: urlmatch.UrlPattern = None) -> bool: + def remove(self, pattern=None): """Remove the value with the given pattern. If a matching pattern was removed, True is returned. @@ -143,11 +131,11 @@ class Values: self._values = [v for v in self._values if v.pattern != pattern] return old_len != len(self._values) - def clear(self) -> None: + def clear(self): """Clear all customization for this value.""" self._values = [] - def _get_fallback(self, fallback: typing.Any) -> typing.Any: + def _get_fallback(self, fallback): """Get the fallback global/default value.""" for scoped in self._values: if scoped.pattern is None: @@ -158,8 +146,7 @@ class Values: else: return UNSET - def get_for_url(self, url: QUrl = None, *, - fallback: bool = True) -> typing.Any: + def get_for_url(self, url=None, *, fallback=True): """Get a config value, falling back when needed. This first tries to find a value matching the URL (if given). @@ -178,9 +165,7 @@ class Values: return self._get_fallback(fallback) - def get_for_pattern(self, - pattern: typing.Optional[urlmatch.UrlPattern], *, - fallback: bool = True) -> typing.Any: + def get_for_pattern(self, pattern, *, fallback=True): """Get a value only if it's been overridden for the given pattern. This is useful when showing values to the user. From 13dac9eef5b43b0dafe757af7056cbc508a0014f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 15:36:21 +0100 Subject: [PATCH 162/258] Revert "Revert "Add types for most of qutebrowser.config"" This reverts commit 7494d238ce6248a217ab958753777ed093d9411e. --- mypy.ini | 36 ++++++ qutebrowser/config/config.py | 184 +++++++++++++++++---------- qutebrowser/config/configcommands.py | 97 ++++++++------ qutebrowser/config/configdata.py | 59 ++++++--- qutebrowser/config/configdiff.py | 7 +- qutebrowser/config/configexc.py | 35 +++-- qutebrowser/config/configfiles.py | 114 ++++++++++------- qutebrowser/config/configinit.py | 20 +-- qutebrowser/config/configutils.py | 49 ++++--- 9 files changed, 385 insertions(+), 216 deletions(-) diff --git a/mypy.ini b/mypy.ini index 288cc6515..be5424327 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,3 +61,39 @@ disallow_incomplete_defs = True [mypy-qutebrowser.commands.cmdutils] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.config.config] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcache] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcommands] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdata] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdiff] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configexc] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configfiles] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configinit] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configutils] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1d7e34345..44bf3ca77 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,18 +23,21 @@ import copy import contextlib import functools import typing +from typing import Any -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl from qutebrowser.config import configdata, configexc, configutils -from qutebrowser.utils import utils, log, jinja +from qutebrowser.utils import utils, log, jinja, urlmatch from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: - # pylint: disable=unused-import - from qutebrowser.config import configcache # pragma: no cover +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from typing import Tuple, MutableMapping + from qutebrowser.config import configcache, configfiles + from qutebrowser.misc import savemanager # An easy way to access the config from other code via config.val.foo val = typing.cast('ConfigContainer', None) @@ -61,7 +64,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name _function: Whether a function rather than a method is decorated. """ - def __init__(self, option, function=False): + def __init__(self, option: str, function: bool = False) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -74,7 +77,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name self._function = function change_filters.append(self) - def validate(self): + def validate(self) -> None: """Make sure the configured option or prefix exists. We can't do this in __init__ as configdata isn't ready yet. @@ -83,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): + 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. @@ -96,7 +99,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func): + def __call__(self, func: typing.Callable) -> typing.Callable: """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -114,20 +117,21 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def wrapper(option=None): + def func_wrapper(option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func() return None + return func_wrapper else: @functools.wraps(func) - def wrapper(wrapper_self, option=None): + def meth_wrapper(wrapper_self: typing.Any, + option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func(wrapper_self) return None - - return wrapper + return meth_wrapper class KeyConfig: @@ -140,17 +144,22 @@ class KeyConfig: _config: The Config object to be used. """ - def __init__(self, config): + _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]] + + def __init__(self, config: 'Config') -> None: self._config = config - def _validate(self, key, mode): + def _validate(self, key: keyutils.KeySequence, mode: str) -> None: """Validate the given key and mode.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - def get_bindings_for(self, mode): + def get_bindings_for( + self, + mode: str + ) -> typing.Dict[keyutils.KeySequence, str]: """Get the combined bindings for the given mode.""" bindings = dict(val.bindings.default[mode]) for key, binding in val.bindings.commands[mode].items(): @@ -160,9 +169,9 @@ class KeyConfig: bindings[key] = binding return bindings - def get_reverse_bindings_for(self, mode): + def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': """Get a dict of commands to a list of bindings for the mode.""" - cmd_to_keys = {} + cmd_to_keys = {} # type: KeyConfig._ReverseBindings bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): @@ -175,7 +184,10 @@ class KeyConfig: cmd_to_keys[cmd].insert(0, str(seq)) return cmd_to_keys - def get_command(self, key, mode, default=False): + def get_command(self, + key: keyutils.KeySequence, + mode: str, + default: bool = False) -> str: """Get the command for a given key (or None).""" self._validate(key, mode) if default: @@ -184,7 +196,11 @@ class KeyConfig: bindings = self.get_bindings_for(mode) return bindings.get(key, None) - def bind(self, key, command, *, mode, save_yaml=False): + def bind(self, + key: keyutils.KeySequence, + command: str, *, + mode: str, + save_yaml: bool = False) -> None: """Add a new binding from key to command.""" if command is not None and not command.strip(): raise configexc.KeybindingError( @@ -192,8 +208,8 @@ class KeyConfig: 'mode'.format(key, mode)) self._validate(key, mode) - log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( - key, command, mode)) + log.keyboard.vdebug( # type: ignore + "Adding binding {} -> {} in mode {}.".format(key, command, mode)) bindings = self._config.get_mutable_obj('bindings.commands') if mode not in bindings: @@ -201,7 +217,10 @@ class KeyConfig: bindings[mode][str(key)] = command self._config.update_mutables(save_yaml=save_yaml) - def bind_default(self, key, *, mode='normal', save_yaml=False): + def bind_default(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Restore a default keybinding.""" self._validate(key, mode) @@ -213,7 +232,10 @@ class KeyConfig: "Can't find binding '{}' in {} mode".format(key, mode)) self._config.update_mutables(save_yaml=save_yaml) - def unbind(self, key, *, mode='normal', save_yaml=False): + def unbind(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Unbind the given key in the given mode.""" self._validate(key, mode) @@ -254,24 +276,27 @@ class Config(QObject): MUTABLE_TYPES = (dict, list) changed = pyqtSignal(str) - def __init__(self, yaml_config, parent=None): + def __init__(self, + yaml_config: 'configfiles.YamlConfig', + parent: QObject = None) -> None: super().__init__(parent) self.changed.connect(_render_stylesheet.cache_clear) - self._mutables = {} + self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] self._yaml = yaml_config self._init_values() - def _init_values(self): + def _init_values(self) -> None: """Populate the self._values dict.""" - self._values = {} + self._values = {} # type: typing.Mapping for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -279,7 +304,10 @@ class Config(QObject): """ self._yaml.init_save_manager(save_manager) - def _set_value(self, opt, value, pattern=None): + def _set_value(self, + opt: 'configdata.Option', + value: Any, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: @@ -294,12 +322,12 @@ class Config(QObject): log.config.debug("Config option changed: {} = {}".format( opt.name, value)) - def _check_yaml(self, opt, save_yaml): + def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None: """Make sure the given option may be set in autoconfig.yml.""" if save_yaml and opt.no_autoconfig: raise configexc.NoAutoconfigError(opt.name) - def read_yaml(self): + def read_yaml(self) -> None: """Read the YAML settings from self._yaml.""" self._yaml.load() for values in self._yaml: @@ -307,7 +335,7 @@ class Config(QObject): self._set_value(values.opt, scoped.value, pattern=scoped.pattern) - def get_opt(self, name): + def get_opt(self, name: str) -> 'configdata.Option': """Get a configdata.Option object for the given setting.""" try: return configdata.DATA[name] @@ -318,7 +346,10 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, name, url=None, *, fallback=True): + def get(self, + name: str, + url: QUrl = None, *, + fallback: bool = True) -> Any: """Get the given setting converted for Python code. Args: @@ -328,7 +359,7 @@ class Config(QObject): obj = self.get_obj(name, url=url, fallback=fallback) return opt.typ.to_py(obj) - def _maybe_copy(self, value): + def _maybe_copy(self, value: Any) -> Any: """Copy the value if it could potentially be mutated.""" if isinstance(value, self.MUTABLE_TYPES): # For mutable objects, create a copy so we don't accidentally @@ -339,7 +370,10 @@ class Config(QObject): assert value.__hash__ is not None, value return value - def get_obj(self, name, *, url=None, fallback=True): + def get_obj(self, + name: str, *, + url: QUrl = None, + fallback: bool = True) -> Any: """Get the given setting as object (for YAML/config.py). Note that the returned values are not watched for mutation. @@ -349,7 +383,10 @@ class Config(QObject): value = self._values[name].get_for_url(url, fallback=fallback) return self._maybe_copy(value) - def get_obj_for_pattern(self, name, *, pattern): + def get_obj_for_pattern( + self, name: str, *, + pattern: typing.Optional[urlmatch.UrlPattern] + ) -> Any: """Get the given setting as object (for YAML/config.py). This gets the overridden value for a given pattern, or @@ -359,7 +396,8 @@ class Config(QObject): value = self._values[name].get_for_pattern(pattern, fallback=False) return self._maybe_copy(value) - def get_mutable_obj(self, name, *, pattern=None): + def get_mutable_obj(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> Any: """Get an object which can be mutated, e.g. in a config.py. If a pattern is given, return the value for that pattern. @@ -384,7 +422,8 @@ class Config(QObject): return copy_value - def get_str(self, name, *, pattern=None): + def get_str(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> str: """Get the given setting as string. If a pattern is given, get the setting for the given pattern or @@ -395,7 +434,10 @@ class Config(QObject): value = values.get_for_pattern(pattern) return opt.typ.to_str(value) - def set_obj(self, name, value, *, pattern=None, save_yaml=False): + def set_obj(self, name: str, + value: Any, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. @@ -406,7 +448,10 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, value, pattern=pattern) - def set_str(self, name, value, *, pattern=None, save_yaml=False): + def set_str(self, name: str, + value: str, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a string. If save_yaml=True is given, store the new value to YAML. @@ -421,7 +466,9 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, converted, pattern=pattern) - def unset(self, name, *, save_yaml=False, pattern=None): + def unset(self, name: str, *, + save_yaml: bool = False, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting back to its default.""" opt = self.get_opt(name) self._check_yaml(opt, save_yaml) @@ -432,7 +479,7 @@ class Config(QObject): if save_yaml: self._yaml.unset(name, pattern=pattern) - def clear(self, *, save_yaml=False): + def clear(self, *, save_yaml: bool = False) -> None: """Clear all settings in the config. If save_yaml=True is given, also remove all customization from the YAML @@ -446,7 +493,7 @@ class Config(QObject): if save_yaml: self._yaml.clear() - def update_mutables(self, *, save_yaml=False): + def update_mutables(self, *, save_yaml: bool = False) -> None: """Update mutable settings if they changed. Every time someone calls get_obj() on a mutable object, we save a @@ -461,7 +508,7 @@ class Config(QObject): self.set_obj(name, new_value, save_yaml=save_yaml) self._mutables = {} - def dump_userconfig(self): + def dump_userconfig(self) -> str: """Get the part of the config which was changed by the user. Return: @@ -490,7 +537,10 @@ class ConfigContainer: _pattern: The URL pattern to be used. """ - def __init__(self, config, configapi=None, prefix='', pattern=None): + def __init__(self, config: Config, + configapi: 'configfiles.ConfigAPI' = None, + prefix: str = '', + pattern: urlmatch.UrlPattern = None) -> None: self._config = config self._prefix = prefix self._configapi = configapi @@ -498,13 +548,13 @@ class ConfigContainer: if configapi is None and pattern is not None: raise TypeError("Can't use pattern without configapi!") - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, constructor=True, config=self._config, configapi=self._configapi, prefix=self._prefix, pattern=self._pattern) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: try: yield except configexc.Error as e: @@ -513,7 +563,7 @@ class ConfigContainer: text = "While {} '{}'".format(action, name) self._configapi.errors.append(configexc.ConfigErrorDesc(text, e)) - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: """Get an option or a new ConfigContainer with the added prefix. If we get an option which exists, we return the value for it. @@ -540,7 +590,7 @@ class ConfigContainer: return self._config.get_mutable_obj( name, pattern=self._pattern) - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value: Any) -> None: """Set the given option in the config.""" if attr.startswith('_'): super().__setattr__(attr, value) @@ -550,7 +600,7 @@ class ConfigContainer: with self._handle_error('setting', name): self._config.set_obj(name, value, pattern=self._pattern) - def _join(self, attr): + def _join(self, attr: str) -> str: """Get the prefix joined with the given attribute.""" if self._prefix: return '{}.{}'.format(self._prefix, attr) @@ -558,8 +608,10 @@ class ConfigContainer: return attr -def set_register_stylesheet(obj, *, stylesheet=None, update=True): - """Set the stylesheet for an object based on it's STYLESHEET attribute. +def set_register_stylesheet(obj: QObject, *, + stylesheet: str = None, + update: bool = True) -> None: + """Set the stylesheet for an object. Also, register an update when the config is changed. @@ -574,7 +626,7 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True): @functools.lru_cache() -def _render_stylesheet(stylesheet): +def _render_stylesheet(stylesheet: str) -> str: """Render the given stylesheet jinja template.""" with jinja.environment.no_autoescape(): template = jinja.environment.from_string(stylesheet) @@ -590,7 +642,9 @@ class StyleSheetObserver(QObject): _stylesheet: The stylesheet template to use. """ - def __init__(self, obj, stylesheet, update): + def __init__(self, obj: QObject, + stylesheet: typing.Optional[str], + update: bool) -> None: super().__init__() self._obj = obj self._update = update @@ -599,11 +653,11 @@ class StyleSheetObserver(QObject): if self._update: self.setParent(self._obj) if stylesheet is None: - self._stylesheet = obj.STYLESHEET + self._stylesheet = obj.STYLESHEET # type: str else: self._stylesheet = stylesheet - def _get_stylesheet(self): + def _get_stylesheet(self) -> str: """Format a stylesheet based on a template. Return: @@ -612,19 +666,15 @@ class StyleSheetObserver(QObject): return _render_stylesheet(self._stylesheet) @pyqtSlot() - def _update_stylesheet(self): + def _update_stylesheet(self) -> None: """Update the stylesheet for obj.""" self._obj.setStyleSheet(self._get_stylesheet()) - def register(self): - """Do a first update and listen for more. - - Args: - update: if False, don't listen for future updates. - """ + def register(self) -> None: + """Do a first update and listen for more.""" qss = self._get_stylesheet() - log.config.vdebug("stylesheet for {}: {}".format( - self._obj.__class__.__name__, qss)) + log.config.vdebug( # type: ignore + "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) self._obj.setStyleSheet(qss) if self._update: instance.changed.connect(self._update_stylesheet) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 574bc06af..0ee77fcc9 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -19,6 +19,7 @@ """Commands related to the configuration.""" +import typing import os.path import contextlib @@ -31,24 +32,34 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config.config import Config, KeyConfig + class ConfigCommands: """qutebrowser commands related to the configuration.""" - def __init__(self, config, keyconfig): + def __init__(self, + config: 'Config', + keyconfig: 'KeyConfig') -> None: self._config = config self._keyconfig = keyconfig @contextlib.contextmanager - def _handle_config_error(self): + def _handle_config_error(self) -> typing.Iterator[None]: """Catch errors in set_command and raise CommandError.""" try: yield except configexc.Error as e: raise cmdutils.CommandError(str(e)) - def _parse_pattern(self, pattern): + def _parse_pattern( + self, + pattern: typing.Optional[str] + ) -> typing.Optional[urlmatch.UrlPattern]: """Parse a pattern string argument to a pattern.""" if pattern is None: return None @@ -59,14 +70,15 @@ class ConfigCommands: raise cmdutils.CommandError("Error while parsing {}: {}" .format(pattern, str(e))) - def _parse_key(self, key): + def _parse_key(self, key: str) -> keyutils.KeySequence: """Parse a key argument.""" try: return keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: raise cmdutils.CommandError(str(e)) - def _print_value(self, option, pattern): + def _print_value(self, option: str, + pattern: typing.Optional[urlmatch.UrlPattern]) -> None: """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) @@ -81,8 +93,9 @@ class ConfigCommands: @cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('pattern', flag='u') - def set(self, win_id, option=None, value=None, temp=False, print_=False, - *, pattern=None): + def set(self, win_id: int, option: str = None, value: str = None, + temp: bool = False, print_: bool = False, + *, pattern: str = None) -> None: """Set an option. If the option name ends with '?' or no value is provided, the @@ -108,28 +121,28 @@ class ConfigCommands: raise cmdutils.CommandError("Toggling values was moved to the " ":config-cycle command") - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) if option.endswith('?') and option != '?': - self._print_value(option[:-1], pattern=pattern) + self._print_value(option[:-1], pattern=parsed_pattern) return with self._handle_config_error(): if value is None: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) else: - self._config.set_str(option, value, pattern=pattern, + self._config.set_str(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) - def bind(self, win_id, key=None, command=None, *, mode='normal', - default=False): + def bind(self, win_id: str, key: str = None, command: str = None, *, + mode: str = 'normal', default: bool = False) -> None: """Bind a key to a command. If no command is given, show the current binding for the given key. @@ -174,7 +187,7 @@ class ConfigCommands: self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands') - def unbind(self, key, *, mode='normal'): + def unbind(self, key: str, *, mode: str = 'normal') -> None: """Unbind a keychain. Args: @@ -191,8 +204,9 @@ class ConfigCommands: @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('pattern', flag='u') - def config_cycle(self, option, *values, pattern=None, temp=False, - print_=False): + def config_cycle(self, option: str, *values: str, + pattern: str = None, + temp: bool = False, print_: bool = False) -> None: """Cycle an option between multiple values. Args: @@ -202,15 +216,15 @@ class ConfigCommands: temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) with self._handle_config_error(): opt = self._config.get_opt(option) - old_value = self._config.get_obj_for_pattern(option, - pattern=pattern) + old_value = self._config.get_obj_for_pattern( + option, pattern=parsed_pattern) if not values and isinstance(opt.typ, configtypes.Bool): - values = ['true', 'false'] + values = ('true', 'false') if len(values) < 2: raise cmdutils.CommandError("Need at least two values for " @@ -219,25 +233,25 @@ class ConfigCommands: # Use the next valid value from values, or the first if the current # value does not appear in the list with self._handle_config_error(): - values = [opt.typ.from_str(val) for val in values] + cycle_values = [opt.typ.from_str(val) for val in values] try: - idx = values.index(old_value) - idx = (idx + 1) % len(values) - value = values[idx] + idx = cycle_values.index(old_value) + idx = (idx + 1) % len(cycle_values) + value = cycle_values[idx] except ValueError: - value = values[0] + value = cycle_values[0] with self._handle_config_error(): - self._config.set_obj(option, value, pattern=pattern, + self._config.set_obj(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.customized_option) - def config_unset(self, option, temp=False): + def config_unset(self, option: str, temp: bool = False) -> None: """Unset an option. This sets an option back to its default and removes it from @@ -252,7 +266,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_add(self, option, value, temp=False): + def config_list_add(self, option: str, value: str, + temp: bool = False) -> None: """Append a value to a config option that is a list. Args: @@ -273,7 +288,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_add(self, option, key, value, temp=False, replace=False): + def config_dict_add(self, option: str, key: str, value: str, + temp: bool = False, replace: bool = False) -> None: """Add a key/value pair to a dictionary option. Args: @@ -302,7 +318,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_remove(self, option, value, temp=False): + def config_list_remove(self, option: str, value: str, + temp: bool = False) -> None: """Remove a value from a list. Args: @@ -329,7 +346,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_remove(self, option, key, temp=False): + def config_dict_remove(self, option: str, key: str, + temp: bool = False) -> None: """Remove a key from a dict. Args: @@ -354,7 +372,7 @@ class ConfigCommands: self._config.update_mutables(save_yaml=not temp) @cmdutils.register(instance='config-commands') - def config_clear(self, save=False): + def config_clear(self, save: bool = False) -> None: """Set all settings back to their default. Args: @@ -364,7 +382,7 @@ class ConfigCommands: self._config.clear(save_yaml=save) @cmdutils.register(instance='config-commands') - def config_source(self, filename=None, clear=False): + def config_source(self, filename: str = None, clear: bool = False) -> None: """Read a config.py file. Args: @@ -386,13 +404,13 @@ class ConfigCommands: raise cmdutils.CommandError(e) @cmdutils.register(instance='config-commands') - def config_edit(self, no_source=False): + def config_edit(self, no_source: bool = False) -> None: """Open the config.py file in the editor. Args: no_source: Don't re-source the config file after editing. """ - def on_file_updated(): + def on_file_updated() -> None: """Source the new config when editing finished. This can't use cmdutils.CommandError as it's run async. @@ -410,7 +428,8 @@ class ConfigCommands: ed.edit_file(filename) @cmdutils.register(instance='config-commands') - def config_write_py(self, filename=None, force=False, defaults=False): + def config_write_py(self, filename: str = None, + force: bool = False, defaults: bool = False) -> None: """Write the current configuration to a config.py file. Args: @@ -429,13 +448,13 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) + options = [] # type: typing.List if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] bindings = dict(configdata.DATA['bindings.default'].default) commented = True else: - options = [] for values in self._config: for scoped in values: options.append((scoped.pattern, values.opt, scoped.value)) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index dace0772a..c93032387 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -24,14 +24,18 @@ Module attributes: DATA: A dict of Option objects after init() has been called. """ +import typing +from typing import Optional # pylint: disable=unused-import import functools import attr from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils -DATA = None -MIGRATIONS = None +DATA = typing.cast(typing.Mapping[str, 'Option'], None) +MIGRATIONS = typing.cast('Migrations', None) + +_BackendDict = typing.Mapping[str, typing.Union[str, bool]] @attr.s @@ -42,15 +46,15 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name = attr.ib() - typ = attr.ib() - default = attr.ib() - backends = attr.ib() - raw_backends = attr.ib() - description = attr.ib() - supports_pattern = attr.ib(default=False) - restart = attr.ib(default=False) - no_autoconfig = attr.ib(default=False) + name = attr.ib() # type: str + typ = attr.ib() # type: configtypes.BaseType + default = attr.ib() # type: typing.Any + backends = attr.ib() # type: typing.Iterable[usertypes.Backend] + raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]] + description = attr.ib() # type: str + supports_pattern = attr.ib(default=False) # type: bool + restart = attr.ib(default=False) # type: bool + no_autoconfig = attr.ib(default=False) # type: bool @attr.s @@ -63,11 +67,13 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed = attr.ib(default=attr.Factory(dict)) - deleted = attr.ib(default=attr.Factory(list)) + renamed = attr.ib( + default=attr.Factory(dict)) # type: typing.Dict[str, str] + deleted = attr.ib( + default=attr.Factory(list)) # type: typing.List[str] -def _raise_invalid_node(name, what, node): +def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: """Raise an exception for an invalid configdata YAML node. Args: @@ -79,13 +85,16 @@ def _raise_invalid_node(name, what, node): name, what, node)) -def _parse_yaml_type(name, node): +def _parse_yaml_type( + name: str, + node: typing.Union[str, typing.Mapping[str, typing.Any]], +) -> configtypes.BaseType: if isinstance(node, str): # e.g: # type: Bool # -> create the type object without any arguments type_name = node - kwargs = {} + kwargs = {} # type: typing.MutableMapping[str, typing.Any] elif isinstance(node, dict): # e.g: # type: @@ -123,7 +132,10 @@ def _parse_yaml_type(name, node): type_name, node, e)) -def _parse_yaml_backends_dict(name, node): +def _parse_yaml_backends_dict( + name: str, + node: _BackendDict, +) -> typing.Sequence[usertypes.Backend]: """Parse a dict definition for backends. Example: @@ -160,7 +172,10 @@ def _parse_yaml_backends_dict(name, node): return backends -def _parse_yaml_backends(name, node): +def _parse_yaml_backends( + name: str, + node: typing.Union[None, str, _BackendDict], +) -> typing.Sequence[usertypes.Backend]: """Parse a backend node in the yaml. It can have one of those four forms: @@ -187,7 +202,9 @@ def _parse_yaml_backends(name, node): raise utils.Unreachable -def _read_yaml(yaml_data): +def _read_yaml( + yaml_data: str, +) -> typing.Tuple[typing.Mapping[str, Option], Migrations]: """Read config data from a YAML file. Args: @@ -249,12 +266,12 @@ def _read_yaml(yaml_data): @functools.lru_cache(maxsize=256) -def is_valid_prefix(prefix): +def is_valid_prefix(prefix: str) -> bool: """Check whether the given prefix is a valid prefix for some option.""" return any(key.startswith(prefix + '.') for key in DATA) -def init(): +def init() -> None: """Initialize configdata from the YAML file.""" global DATA, MIGRATIONS DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml')) diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 9f8b70a26..ba78f64b4 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -19,6 +19,7 @@ """Code to show a diff of the legacy config format.""" +import typing # pylint: disable=unused-import,useless-suppression import difflib import os.path @@ -727,10 +728,10 @@ scroll right """ -def get_diff(): +def get_diff() -> str: """Get a HTML diff for the old config files.""" - old_conf_lines = [] - old_key_lines = [] + old_conf_lines = [] # type: typing.MutableSequence[str] + old_key_lines = [] # type: typing.MutableSequence[str] for filename, dest in [('qutebrowser.conf', old_conf_lines), ('keys.conf', old_key_lines)]: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index 051ed971a..b1dc04e09 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,9 +19,10 @@ """Exceptions related to config parsing.""" +import typing import attr -from qutebrowser.utils import jinja +from qutebrowser.utils import jinja, usertypes class Error(Exception): @@ -33,7 +34,7 @@ class NoAutoconfigError(Error): """Raised when this option can't be set in autoconfig.yml.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting can only be set in config.py!" .format(name)) @@ -42,7 +43,11 @@ class BackendError(Error): """Raised when this setting is unavailable with the current backend.""" - def __init__(self, name, backend, raw_backends): + def __init__( + self, name: str, + backend: usertypes.Backend, + raw_backends: typing.Optional[typing.Mapping[str, bool]] + ) -> None: if raw_backends is None or not raw_backends[backend.name]: msg = ("The {} setting is not available with the {} backend!" .format(name, backend.name)) @@ -57,7 +62,7 @@ class NoPatternError(Error): """Raised when the given setting does not support URL patterns.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting does not support URL patterns!" .format(name)) @@ -71,7 +76,7 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value, msg): + def __init__(self, value: typing.Any, msg: str) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None @@ -85,7 +90,9 @@ class NoOptionError(Error): """Raised when an option was not found.""" - def __init__(self, option, *, deleted=False, renamed=None): + def __init__(self, option: str, *, + deleted: bool = False, + renamed: str = None) -> None: if deleted: assert renamed is None suffix = ' (this option was removed from qutebrowser)' @@ -109,18 +116,18 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text = attr.ib() - exception = attr.ib() - traceback = attr.ib(None) + text = attr.ib() # type: str + exception = attr.ib() # type: typing.Union[str, Exception] + traceback = attr.ib(None) # type: str - def __str__(self): + def __str__(self) -> str: if self.traceback: return '{} - {}: {}'.format(self.text, self.exception.__class__.__name__, self.exception) return '{}: {}'.format(self.text, self.exception) - def with_text(self, text): + def with_text(self, text: str) -> 'ConfigErrorDesc': """Get a new ConfigErrorDesc with the given text appended.""" return self.__class__(text='{} ({})'.format(self.text, text), exception=self.exception, @@ -131,13 +138,15 @@ class ConfigFileErrors(Error): """Raised when multiple errors occurred inside the config.""" - def __init__(self, basename, errors): + def __init__(self, + basename: str, + errors: typing.Sequence[ConfigErrorDesc]) -> None: super().__init__("Errors occurred while reading {}:\n{}".format( basename, '\n'.join(' {}'.format(e) for e in errors))) self.basename = basename self.errors = errors - def to_html(self): + def to_html(self) -> str: """Get the error texts as a HTML snippet.""" template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index b4c8ea4ec..54ca91488 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -27,6 +27,7 @@ import textwrap import traceback import configparser import contextlib +import typing import yaml from PyQt5.QtCore import pyqtSignal, QObject, QSettings @@ -36,16 +37,21 @@ from qutebrowser.config import configexc, config, configdata, configutils from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch +MYPY = False +if MYPY: + # pylint: disable=unused-import, useless-suppression + from qutebrowser.misc import savemanager + # The StateConfig instance -state = None +state = typing.cast('StateConfig', None) class StateConfig(configparser.ConfigParser): """The "state" file saving various application state.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._filename = os.path.join(standarddir.data(), 'state') self.read(self._filename, encoding='utf-8') @@ -59,7 +65,8 @@ class StateConfig(configparser.ConfigParser): for key in deleted_keys: self['general'].pop(key, None) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -67,7 +74,7 @@ class StateConfig(configparser.ConfigParser): """ save_manager.add_saveable('state-config', self._save) - def _save(self): + def _save(self) -> None: """Save the state file to the configured location.""" with open(self._filename, 'w', encoding='utf-8') as f: self.write(f) @@ -84,17 +91,20 @@ class YamlConfig(QObject): VERSION = 2 changed = pyqtSignal() - def __init__(self, parent=None): + _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), 'autoconfig.yml') - self._dirty = None + self._dirty = False - self._values = {} + self._values = {} # type: typing.Dict[str, configutils.Values] for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -102,21 +112,21 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def _mark_changed(self): + def _mark_changed(self) -> None: """Mark the YAML config as changed.""" self._dirty = True self.changed.emit() - def _save(self): + def _save(self) -> None: """Save the settings to the YAML file if they've changed.""" if not self._dirty: return - settings = {} + settings = {} # type: YamlConfig._SettingsType for name, values in sorted(self._values.items()): if not values: continue @@ -135,7 +145,10 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) - def _pop_object(self, yaml_data, key, typ): + def _pop_object(self, + yaml_data: typing.Any, + key: str, + typ: type) -> typing.Any: """Get a global object from the given data.""" if not isinstance(yaml_data, dict): desc = configexc.ConfigErrorDesc("While loading data", @@ -158,7 +171,7 @@ class YamlConfig(QObject): return data - def load(self): + def load(self) -> None: """Load configuration from the configured YAML file.""" try: with open(self._filename, 'r', encoding='utf-8') as f: @@ -189,18 +202,19 @@ class YamlConfig(QObject): self._validate(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data): + def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType': """Load the settings from the settings: key.""" return self._pop_object(yaml_data, 'settings', dict) - def _load_legacy_settings_object(self, yaml_data): + def _load_legacy_settings_object(self, + yaml_data: typing.Any) -> '_SettingsType': data = self._pop_object(yaml_data, 'global', dict) settings = {} for name, value in data.items(): settings[name] = {'global': value} return settings - def _build_values(self, settings): + def _build_values(self, settings: typing.Mapping) -> None: """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -233,7 +247,8 @@ class YamlConfig(QObject): if errors: raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def _migrate_bool(self, settings, name, true_value, false_value): + def _migrate_bool(self, settings: _SettingsType, name: str, + true_value: str, false_value: str) -> None: """Migrate a boolean in the settings.""" if name in settings: for scope, val in settings[name].items(): @@ -241,7 +256,7 @@ class YamlConfig(QObject): settings[name][scope] = true_value if val else false_value self._mark_changed() - def _handle_migrations(self, settings): + def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType': """Migrate older configs to the newest format.""" # Simple renamed/deleted options for name in list(settings): @@ -299,7 +314,7 @@ class YamlConfig(QObject): return settings - def _validate(self, settings): + def _validate(self, settings: _SettingsType) -> None: """Make sure all settings exist.""" unknown = [] for name in settings: @@ -312,18 +327,19 @@ class YamlConfig(QObject): for e in sorted(unknown)] raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def set_obj(self, name, value, *, pattern=None): + def set_obj(self, name: str, value: typing.Any, *, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting to the given value.""" self._values[name].add(value, pattern) self._mark_changed() - def unset(self, name, *, pattern=None): + def unset(self, name: str, *, pattern: urlmatch.UrlPattern = None) -> None: """Remove the given option name if it's configured.""" changed = self._values[name].remove(pattern) if changed: self._mark_changed() - def clear(self): + def clear(self) -> None: """Clear all values from the YAML file.""" for values in self._values.values(): values.clear() @@ -346,15 +362,15 @@ class ConfigAPI: datadir: The qutebrowser data directory, as pathlib.Path. """ - def __init__(self, conf, keyconfig): + def __init__(self, conf: config.Config, keyconfig: config.KeyConfig): self._config = conf self._keyconfig = keyconfig - self.errors = [] + self.errors = [] # type: typing.List[configexc.ConfigErrorDesc] self.configdir = pathlib.Path(standarddir.config()) self.datadir = pathlib.Path(standarddir.data()) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: """Catch config-related exceptions and save them in self.errors.""" try: yield @@ -372,40 +388,40 @@ class ConfigAPI: text = "While {} '{}' and parsing key".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) - def finalize(self): + def finalize(self) -> None: """Do work which needs to be done after reading config.py.""" self._config.update_mutables() - def load_autoconfig(self): + def load_autoconfig(self) -> None: """Load the autoconfig.yml file which is used for :set/:bind/etc.""" with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name, pattern=None): + def get(self, name: str, pattern: str = None) -> typing.Any: """Get a setting value from the config, optionally with a pattern.""" with self._handle_error('getting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None return self._config.get_mutable_obj(name, pattern=urlpattern) - def set(self, name, value, pattern=None): + def set(self, name: str, value: typing.Any, pattern: str = None) -> None: """Set a setting value in the config, optionally with a pattern.""" with self._handle_error('setting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None self._config.set_obj(name, value, pattern=urlpattern) - def bind(self, key, command, mode='normal'): + def bind(self, key: str, command: str, mode: str = 'normal') -> None: """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.bind(seq, command, mode=mode) - def unbind(self, key, mode='normal'): + def unbind(self, key: str, mode: str = 'normal') -> None: """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.unbind(seq, mode=mode) - def source(self, filename): + def source(self, filename: str) -> None: """Read the given config file from disk.""" if not os.path.isabs(filename): filename = str(self.configdir / filename) @@ -416,7 +432,7 @@ class ConfigAPI: self.errors += e.errors @contextlib.contextmanager - def pattern(self, pattern): + def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]: """Get a ConfigContainer for the given pattern.""" # We need to propagate the exception so we don't need to return # something. @@ -430,17 +446,21 @@ class ConfigPyWriter: """Writer for config.py files from given settings.""" - def __init__(self, options, bindings, *, commented): + def __init__( + self, + options: typing.List, + bindings: typing.MutableMapping[str, typing.Mapping[str, str]], *, + commented: bool) -> None: self._options = options self._bindings = bindings self._commented = commented - def write(self, filename): + def write(self, filename: str) -> None: """Write the config to the given file.""" with open(filename, 'w', encoding='utf-8') as f: f.write('\n'.join(self._gen_lines())) - def _line(self, line): + def _line(self, line: str) -> str: """Get an (optionally commented) line.""" if self._commented: if line.startswith('#'): @@ -450,7 +470,7 @@ class ConfigPyWriter: else: return line - def _gen_lines(self): + def _gen_lines(self) -> typing.Iterator[str]: """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -459,7 +479,7 @@ class ConfigPyWriter: yield from self._gen_options() yield from self._gen_bindings() - def _gen_header(self): + def _gen_header(self) -> typing.Iterator[str]: """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") yield self._line("# Documentation:") @@ -481,7 +501,7 @@ class ConfigPyWriter: yield self._line("# config.load_autoconfig()") yield '' - def _gen_options(self): + def _gen_options(self) -> typing.Iterator[str]: """Generate the options part of the config.""" for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: @@ -509,7 +529,7 @@ class ConfigPyWriter: opt.name, value, str(pattern))) yield '' - def _gen_bindings(self): + def _gen_bindings(self) -> typing.Iterator[str]: """Generate the bindings part of the config.""" normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: @@ -527,7 +547,7 @@ class ConfigPyWriter: yield '' -def read_config_py(filename, raising=False): +def read_config_py(filename: str, raising: bool = False) -> None: """Read a config.py file. Arguments; @@ -543,8 +563,8 @@ def read_config_py(filename, raising=False): basename = os.path.basename(filename) module = types.ModuleType('config') - module.config = api - module.c = container + module.config = api # type: ignore + module.c = container # type: ignore module.__file__ = filename try: @@ -589,7 +609,7 @@ def read_config_py(filename, raising=False): raise configexc.ConfigFileErrors('config.py', api.errors) -def read_autoconfig(): +def read_autoconfig() -> None: """Read the autoconfig.yml file.""" try: config.instance.read_yaml() @@ -601,7 +621,7 @@ def read_autoconfig(): @contextlib.contextmanager -def saved_sys_properties(): +def saved_sys_properties() -> typing.Iterator[None]: """Save various sys properties such as sys.path and sys.modules.""" old_path = sys.path.copy() old_modules = sys.modules.copy() @@ -614,7 +634,7 @@ def saved_sys_properties(): del sys.modules[module] -def init(): +def init() -> None: """Initialize config storage not related to the main config.""" global state state = StateConfig() diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index de9651064..ff0fd0e41 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -19,8 +19,10 @@ """Initialization of the configuration.""" +import argparse import os.path import sys +import typing from PyQt5.QtWidgets import QMessageBox @@ -30,14 +32,14 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, qtutils) from qutebrowser.config import configcache -from qutebrowser.misc import msgbox, objects +from qutebrowser.misc import msgbox, objects, savemanager # Error which happened during init, so we can show a message box. _init_errors = None -def early_init(args): +def early_init(args: argparse.Namespace) -> None: """Initialize the part of the config which works without a QApplication.""" configdata.init() @@ -85,7 +87,7 @@ def early_init(args): _init_envvars() -def _init_envvars(): +def _init_envvars() -> None: """Initialize environment variables which need to be set early.""" if objects.backend == usertypes.Backend.QtWebEngine: software_rendering = config.val.qt.force_software_rendering @@ -107,7 +109,7 @@ def _init_envvars(): @config.change_filter('fonts.monospace', function=True) -def _update_monospace_fonts(): +def _update_monospace_fonts() -> None: """Update all fonts if fonts.monospace was set.""" configtypes.Font.monospace_fonts = config.val.fonts.monospace for name, opt in configdata.DATA.items(): @@ -123,7 +125,7 @@ def _update_monospace_fonts(): config.instance.changed.emit(name) -def get_backend(args): +def get_backend(args: argparse.Namespace) -> usertypes.Backend: """Find out what backend to use based on available libraries.""" str_to_backend = { 'webkit': usertypes.Backend.QtWebKit, @@ -136,7 +138,7 @@ def get_backend(args): return str_to_backend[config.val.backend] -def late_init(save_manager): +def late_init(save_manager: savemanager.SaveManager) -> None: """Initialize the rest of the config after the QApplication is created.""" global _init_errors if _init_errors is not None: @@ -152,7 +154,7 @@ def late_init(save_manager): configfiles.state.init_save_manager(save_manager) -def qt_args(namespace): +def qt_args(namespace: argparse.Namespace) -> typing.List[str]: """Get the Qt QApplication arguments based on an argparse namespace. Args: @@ -178,7 +180,7 @@ def qt_args(namespace): return argv -def _qtwebengine_args(): +def _qtwebengine_args() -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to @@ -224,7 +226,7 @@ def _qtwebengine_args(): 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } - } + } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 96fc0f02d..9d4dc94ef 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -21,11 +21,19 @@ """Utilities and data structures used by various config code.""" -import attr +import typing -from qutebrowser.utils import utils +import attr +from PyQt5.QtCore import QUrl + +from qutebrowser.utils import utils, urlmatch from qutebrowser.config import configexc +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config import configdata + class _UnsetObject: @@ -33,7 +41,7 @@ class _UnsetObject: __slots__ = () - def __repr__(self): + def __repr__(self) -> str: return '' @@ -50,8 +58,8 @@ class ScopedValue: pattern: The UrlPattern for the value, or None for global values. """ - value = attr.ib() - pattern = attr.ib() + value = attr.ib() # type: typing.Any + pattern = attr.ib() # type: typing.Optional[urlmatch.UrlPattern] class Values: @@ -73,15 +81,17 @@ class Values: opt: The Option being customized. """ - def __init__(self, opt, values=None): + def __init__(self, + opt: 'configdata.Option', + values: typing.MutableSequence = None) -> None: self.opt = opt self._values = values or [] - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, opt=self.opt, values=self._values, constructor=True) - def __str__(self): + def __str__(self) -> str: """Get the values as human-readable string.""" if not self: return '{}: '.format(self.opt.name) @@ -96,7 +106,7 @@ class Values: scoped.pattern, self.opt.name, str_value)) return '\n'.join(lines) - def __iter__(self): + def __iter__(self) -> typing.Iterator['ScopedValue']: """Yield ScopedValue elements. This yields in "normal" order, i.e. global and then first-set settings @@ -104,23 +114,25 @@ class Values: """ yield from self._values - def __bool__(self): + def __bool__(self) -> bool: """Check whether this value is customized.""" return bool(self._values) - def _check_pattern_support(self, arg): + def _check_pattern_support( + self, arg: typing.Optional[urlmatch.UrlPattern]) -> None: """Make sure patterns are supported if one was given.""" if arg is not None and not self.opt.supports_pattern: raise configexc.NoPatternError(self.opt.name) - def add(self, value, pattern=None): + def add(self, value: typing.Any, + pattern: urlmatch.UrlPattern = None) -> None: """Add a value with the given pattern to the list of values.""" self._check_pattern_support(pattern) self.remove(pattern) scoped = ScopedValue(value, pattern) self._values.append(scoped) - def remove(self, pattern=None): + def remove(self, pattern: urlmatch.UrlPattern = None) -> bool: """Remove the value with the given pattern. If a matching pattern was removed, True is returned. @@ -131,11 +143,11 @@ class Values: self._values = [v for v in self._values if v.pattern != pattern] return old_len != len(self._values) - def clear(self): + def clear(self) -> None: """Clear all customization for this value.""" self._values = [] - def _get_fallback(self, fallback): + def _get_fallback(self, fallback: typing.Any) -> typing.Any: """Get the fallback global/default value.""" for scoped in self._values: if scoped.pattern is None: @@ -146,7 +158,8 @@ class Values: else: return UNSET - def get_for_url(self, url=None, *, fallback=True): + def get_for_url(self, url: QUrl = None, *, + fallback: bool = True) -> typing.Any: """Get a config value, falling back when needed. This first tries to find a value matching the URL (if given). @@ -165,7 +178,9 @@ class Values: return self._get_fallback(fallback) - def get_for_pattern(self, pattern, *, fallback=True): + def get_for_pattern(self, + pattern: typing.Optional[urlmatch.UrlPattern], *, + fallback: bool = True) -> typing.Any: """Get a value only if it's been overridden for the given pattern. This is useful when showing values to the user. From 71f9c5d2061093535f10b1ffa0619a5b1e8131ff Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 15:48:52 +0100 Subject: [PATCH 163/258] Fix handling of annotated varargs in commands When we have something like "*values: str" in :config-cycle, we get a list of values, but the type converter assumes it's a string. We could implement proper conversion of *args, but for now, let's just make sure it's always a string. --- qutebrowser/commands/command.py | 13 +++++++++---- tests/unit/api/test_cmdutils.py | 9 +++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 8d2f5ad57..9883447a8 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -333,13 +333,18 @@ class Command: param: The inspect.Parameter to look at. """ arginfo = self.get_arg_info(param) - if param.annotation is not inspect.Parameter.empty: + if arginfo.value: + # Filled values are passed 1:1 + return None + elif param.kind in [inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD]: + # For *args/**kwargs we only support strings + assert param.annotation in [inspect.Parameter.empty, str], param + return None + elif param.annotation is not inspect.Parameter.empty: return param.annotation elif param.default not in [None, inspect.Parameter.empty]: return type(param.default) - elif arginfo.value or param.kind in [inspect.Parameter.VAR_POSITIONAL, - inspect.Parameter.VAR_KEYWORD]: - return None else: return str diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 5e1389c80..9f01ea7bb 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -154,6 +154,15 @@ class TestRegister: args, kwargs = cmd._get_call_args(win_id=0) fun(*args, **kwargs) + def test_star_args_optional_annotated(self): + @cmdutils.register(star_args_optional=True) + def fun(*args: str): + """Blah.""" + + cmd = objects.commands['fun'] + cmd.namespace = cmd.parser.parse_args([]) + cmd._get_call_args(win_id=0) + @pytest.mark.parametrize('inp, expected', [ (['--arg'], True), (['-a'], True), ([], False)]) def test_flag(self, inp, expected): From 70284704c7efc4a0e787989e04fd0312f5cb717b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Dec 2018 19:15:13 +0100 Subject: [PATCH 164/258] Update certifi from 2018.10.15 to 2018.11.29 --- 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 2ab6e78f0..4ce97a976 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -certifi==2018.10.15 +certifi==2018.11.29 chardet==3.0.4 codecov==2.0.15 coverage==4.5.2 From 2a7a3de8fc40fe9adf36ecc1968b61d1b9c46caf Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Dec 2018 19:15:15 +0100 Subject: [PATCH 165/258] Update certifi from 2018.10.15 to 2018.11.29 --- 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 8329ae0dd..39e731984 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -2,7 +2,7 @@ asn1crypto==0.24.0 astroid==2.1.0 -certifi==2018.10.15 +certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 cryptography==2.4.2 From 953042d75d3b6c1ce09cdb3738405fe6ac6cb17c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Dec 2018 07:28:28 +0100 Subject: [PATCH 166/258] mypy: Disable untyped decorators --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index be5424327..154a877bf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,6 +7,7 @@ python_version = 3.6 warn_redundant_casts = True warn_unused_ignores = True disallow_subclassing_any = True +disallow_untyped_decorators = True ## https://github.com/python/mypy/issues/5957 # warn_unused_configs = True # disallow_untyped_calls = True @@ -14,7 +15,6 @@ disallow_subclassing_any = True ## https://github.com/python/mypy/issues/5954 # disallow_incomplete_defs = True # check_untyped_defs = True -# disallow_untyped_decorators = True # no_implicit_optional = True # warn_return_any = True From 268ad40982466e0985961318e0a6c2b4ae17423f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Dec 2018 15:52:14 +0100 Subject: [PATCH 167/258] Add and enforce types for api.*/components.* --- mypy.ini | 8 +++++ qutebrowser/api/apitypes.py | 3 +- qutebrowser/components/caretcommands.py | 36 +++++++++---------- qutebrowser/components/misccommands.py | 44 +++++++++++++----------- qutebrowser/components/scrollcommands.py | 2 +- qutebrowser/components/zoomcommands.py | 23 +++++++------ 6 files changed, 65 insertions(+), 51 deletions(-) diff --git a/mypy.ini b/mypy.ini index 154a877bf..c61ca1f18 100644 --- a/mypy.ini +++ b/mypy.ini @@ -97,3 +97,11 @@ disallow_incomplete_defs = True [mypy-qutebrowser.config.configutils] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.api.*] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.components.*] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/api/apitypes.py b/qutebrowser/api/apitypes.py index 2ed188724..9fec0a6cb 100644 --- a/qutebrowser/api/apitypes.py +++ b/qutebrowser/api/apitypes.py @@ -21,5 +21,6 @@ # pylint: disable=unused-import from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab -from qutebrowser.browser.webelem import Error as WebElemError +from qutebrowser.browser.webelem import (Error as WebElemError, + AbstractWebElement as WebElement) from qutebrowser.utils.usertypes import ClickTarget, JsWorld diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index fe04f2483..4bab6b6c6 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -26,7 +26,7 @@ from qutebrowser.api import cmdutils, apitypes @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_next_line(tab: apitypes.Tab, count: int = 1): +def move_to_next_line(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the next line. Args: @@ -38,7 +38,7 @@ def move_to_next_line(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_prev_line(tab: apitypes.Tab, count: int = 1): +def move_to_prev_line(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the prev line. Args: @@ -50,7 +50,7 @@ def move_to_prev_line(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_next_char(tab: apitypes.Tab, count: int = 1): +def move_to_next_char(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the next char. Args: @@ -62,7 +62,7 @@ def move_to_next_char(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_prev_char(tab: apitypes.Tab, count: int = 1): +def move_to_prev_char(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the previous char. Args: @@ -74,7 +74,7 @@ def move_to_prev_char(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_end_of_word(tab: apitypes.Tab, count: int = 1): +def move_to_end_of_word(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the end of the word. Args: @@ -86,7 +86,7 @@ def move_to_end_of_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_next_word(tab: apitypes.Tab, count: int = 1): +def move_to_next_word(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the next word. Args: @@ -98,7 +98,7 @@ def move_to_next_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_prev_word(tab: apitypes.Tab, count: int = 1): +def move_to_prev_word(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the previous word. Args: @@ -109,14 +109,14 @@ def move_to_prev_word(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def move_to_start_of_line(tab: apitypes.Tab): +def move_to_start_of_line(tab: apitypes.Tab) -> None: """Move the cursor or selection to the start of the line.""" tab.caret.move_to_start_of_line() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def move_to_end_of_line(tab: apitypes.Tab): +def move_to_end_of_line(tab: apitypes.Tab) -> None: """Move the cursor or selection to the end of line.""" tab.caret.move_to_end_of_line() @@ -124,7 +124,7 @@ def move_to_end_of_line(tab: apitypes.Tab): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1): +def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the start of next block. Args: @@ -136,7 +136,7 @@ def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1): +def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the start of previous block. Args: @@ -148,7 +148,7 @@ def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1): +def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the end of next block. Args: @@ -160,7 +160,7 @@ def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1): +def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None: """Move the cursor or selection to the end of previous block. Args: @@ -171,35 +171,35 @@ def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1): @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def move_to_start_of_document(tab: apitypes.Tab): +def move_to_start_of_document(tab: apitypes.Tab) -> None: """Move the cursor or selection to the start of the document.""" tab.caret.move_to_start_of_document() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def move_to_end_of_document(tab: apitypes.Tab): +def move_to_end_of_document(tab: apitypes.Tab) -> None: """Move the cursor or selection to the end of the document.""" tab.caret.move_to_end_of_document() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def toggle_selection(tab: apitypes.Tab): +def toggle_selection(tab: apitypes.Tab) -> None: """Toggle caret selection mode.""" tab.caret.toggle_selection() @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def drop_selection(tab: apitypes.Tab): +def drop_selection(tab: apitypes.Tab) -> None: """Drop selection and keep selection mode enabled.""" tab.caret.drop_selection() @cmdutils.register() @cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab) -def follow_selected(tab_obj: apitypes.Tab, *, tab=False): +def follow_selected(tab_obj: apitypes.Tab, *, tab: bool = False) -> None: """Follow the selected text. Args: diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 2f9e2f5e5..60715c65b 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -37,7 +37,7 @@ from qutebrowser.api import cmdutils, apitypes, message, config @cmdutils.register(name='reload') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def reloadpage(tab, force=False): +def reloadpage(tab: apitypes.Tab, force: bool = False) -> None: """Reload the current/[count]th tab. Args: @@ -50,7 +50,7 @@ def reloadpage(tab, force=False): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def stop(tab): +def stop(tab: apitypes.Tab) -> None: """Stop loading in the current/[count]th tab. Args: @@ -60,9 +60,9 @@ def stop(tab): tab.stop() -def _print_preview(tab): +def _print_preview(tab: apitypes.Tab) -> None: """Show a print preview.""" - def print_callback(ok): + def print_callback(ok: bool) -> None: if not ok: message.error("Printing failed!") @@ -76,7 +76,7 @@ def _print_preview(tab): diag.exec_() -def _print_pdf(tab, filename): +def _print_pdf(tab: apitypes.Tab, filename: str) -> None: """Print to the given PDF file.""" tab.printing.check_pdf_support() filename = os.path.expanduser(filename) @@ -90,7 +90,9 @@ def _print_pdf(tab, filename): @cmdutils.register(name='print') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) @cmdutils.argument('pdf', flag='f', metavar='file') -def printpage(tab, preview=False, *, pdf=None): +def printpage(tab: apitypes.Tab, + preview: bool = False, *, + pdf: str = None) -> None: """Print the current/[count]th tab. Args: @@ -114,7 +116,7 @@ def printpage(tab, preview=False, *, pdf=None): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def home(tab): +def home(tab: apitypes.Tab) -> None: """Open main startpage in current tab.""" if tab.data.pinned: message.info("Tab is pinned!") @@ -124,7 +126,7 @@ def home(tab): @cmdutils.register(debug=True) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def debug_dump_page(tab, dest, plain=False): +def debug_dump_page(tab: apitypes.Tab, dest: str, plain: bool = False) -> None: """Dump the current page's content to a file. Args: @@ -133,7 +135,7 @@ def debug_dump_page(tab, dest, plain=False): """ dest = os.path.expanduser(dest) - def callback(data): + def callback(data: str) -> None: """Write the data to disk.""" try: with open(dest, 'w', encoding='utf-8') as f: @@ -148,13 +150,13 @@ def debug_dump_page(tab, dest, plain=False): @cmdutils.register(maxsplit=0) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def insert_text(tab, text): +def insert_text(tab: apitypes.Tab, text: str) -> None: """Insert text at cursor position. Args: text: The text to insert. """ - def _insert_text_cb(elem): + def _insert_text_cb(elem: apitypes.WebElement) -> None: if elem is None: message.error("No element focused!") return @@ -170,7 +172,7 @@ def insert_text(tab, text): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('filter_', choices=['id']) -def click_element(tab, filter_: str, value: str, *, +def click_element(tab: apitypes.Tab, filter_: str, value: str, *, target: apitypes.ClickTarget = apitypes.ClickTarget.normal, force_event: bool = False) -> None: @@ -186,7 +188,7 @@ def click_element(tab, filter_: str, value: str, *, target: How to open the clicked element (normal/tab/tab-bg/window). force_event: Force generating a fake click event. """ - def single_cb(elem): + def single_cb(elem: apitypes.WebElement) -> None: """Click a single element.""" if elem is None: message.error("No element found with id {}!".format(value)) @@ -207,7 +209,7 @@ def click_element(tab, filter_: str, value: str, *, @cmdutils.register(debug=True) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def debug_webaction(tab, action, count=1): +def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None: """Execute a webaction. Available actions: @@ -227,7 +229,7 @@ def debug_webaction(tab, action, count=1): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def tab_mute(tab): +def tab_mute(tab: apitypes.Tab) -> None: """Mute/Unmute the current/[count]th tab. Args: @@ -242,12 +244,12 @@ def tab_mute(tab): @cmdutils.register() -def nop(): +def nop() -> None: """Do nothing.""" @cmdutils.register() -def message_error(text): +def message_error(text: str) -> None: """Show an error message in the statusbar. Args: @@ -258,7 +260,7 @@ def message_error(text): @cmdutils.register() @cmdutils.argument('count', value=cmdutils.Value.count) -def message_info(text, count=1): +def message_info(text: str, count: int = 1) -> None: """Show an info message in the statusbar. Args: @@ -270,7 +272,7 @@ def message_info(text, count=1): @cmdutils.register() -def message_warning(text): +def message_warning(text: str) -> None: """Show a warning message in the statusbar. Args: @@ -281,7 +283,7 @@ def message_warning(text): @cmdutils.register(debug=True) @cmdutils.argument('typ', choices=['exception', 'segfault']) -def debug_crash(typ='exception'): +def debug_crash(typ: str = 'exception') -> None: """Crash for debugging purposes. Args: @@ -295,7 +297,7 @@ def debug_crash(typ='exception'): @cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True) -def debug_trace(expr=""): +def debug_trace(expr: str = "") -> None: """Trace executed code via hunter. Args: diff --git a/qutebrowser/components/scrollcommands.py b/qutebrowser/components/scrollcommands.py index 4a6d9cb99..0b8943f2d 100644 --- a/qutebrowser/components/scrollcommands.py +++ b/qutebrowser/components/scrollcommands.py @@ -112,7 +112,7 @@ def scroll_to_perc(tab: apitypes.Tab, count: int = None, @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def scroll_to_anchor(tab: apitypes.Tab, name): +def scroll_to_anchor(tab: apitypes.Tab, name: str) -> None: """Scroll to the given anchor in the document. Args: diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py index 05d2d8481..69f1df1c4 100644 --- a/qutebrowser/components/zoomcommands.py +++ b/qutebrowser/components/zoomcommands.py @@ -25,7 +25,7 @@ from qutebrowser.api import cmdutils, apitypes, message, config @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def zoom_in(tab: apitypes.Tab, count=1, quiet=False): +def zoom_in(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None: """Increase the zoom level for the current tab. Args: @@ -43,7 +43,7 @@ def zoom_in(tab: apitypes.Tab, count=1, quiet=False): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def zoom_out(tab: apitypes.Tab, count=1, quiet=False): +def zoom_out(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None: """Decrease the zoom level for the current tab. Args: @@ -61,7 +61,10 @@ def zoom_out(tab: apitypes.Tab, count=1, quiet=False): @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) @cmdutils.argument('count', value=cmdutils.Value.count) -def zoom(tab: apitypes.Tab, level=None, count=None, quiet=False): +def zoom(tab: apitypes.Tab, + level: str = None, + count: int = None, + quiet: bool = False) -> None: """Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is @@ -75,19 +78,19 @@ def zoom(tab: apitypes.Tab, level=None, count=None, quiet=False): """ if level is not None: try: - level = int(level.rstrip('%')) + int_level = int(level.rstrip('%')) except ValueError: raise cmdutils.CommandError("zoom: Invalid int value {}" .format(level)) if count is not None: - level = count - elif level is None: - level = config.val.zoom.default + int_level = count + elif int_level is None: + int_level = config.val.zoom.default try: - tab.zoom.set_factor(float(level) / 100) + tab.zoom.set_factor(int_level / 100) except ValueError: - raise cmdutils.CommandError("Can't zoom {}%!".format(level)) + raise cmdutils.CommandError("Can't zoom {}%!".format(int_level)) if not quiet: - message.info("Zoom level: {}%".format(int(level)), replace=True) + message.info("Zoom level: {}%".format(int_level), replace=True) From 2cb277afd71a3597190726a797d4a9585ebfad13 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Dec 2018 16:29:44 +0100 Subject: [PATCH 168/258] Fix exception messages in Command The messages weren't updated in ac78039171d6742e2b55aca146a5bab33cd565a1. --- qutebrowser/commands/command.py | 6 +++--- tests/unit/api/test_cmdutils.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 9883447a8..46f92772f 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -157,18 +157,18 @@ class Command: if 'self' in signature.parameters: if self._instance is None: raise TypeError("{} is a class method, but instance was not " - "given!".format(self.name[0])) + "given!".format(self.name)) arg_info = self.get_arg_info(signature.parameters['self']) if arg_info.value is not None: raise TypeError("{}: Can't fill 'self' with value!" .format(self.name)) elif 'self' not in signature.parameters and self._instance is not None: raise TypeError("{} is not a class method, but instance was " - "given!".format(self.name[0])) + "given!".format(self.name)) elif any(param.kind == inspect.Parameter.VAR_KEYWORD for param in signature.parameters.values()): raise TypeError("{}: functions with varkw arguments are not " - "supported!".format(self.name[0])) + "supported!".format(self.name)) def get_arg_info(self, param): """Get an ArgInfo tuple for the given inspect.Parameter.""" diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 9f01ea7bb..4116045ae 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -190,6 +190,27 @@ class TestRegister: args, kwargs = cmd._get_call_args(win_id=0) fun(*args, **kwargs) + def test_self_without_instance(self): + with pytest.raises(TypeError, match="fun is a class method, but " + "instance was not given!"): + @cmdutils.register() + def fun(self): + """Blah.""" + + def test_instance_without_self(self): + with pytest.raises(TypeError, match="fun is not a class method, but " + "instance was given!"): + @cmdutils.register(instance='inst') + def fun(): + """Blah.""" + + def test_var_kw(self): + with pytest.raises(TypeError, match="fun: functions with varkw " + "arguments are not supported!"): + @cmdutils.register() + def fun(**kwargs): + """Blah.""" + def test_partial_arg(self): """Test with only some arguments decorated with @cmdutils.argument.""" @cmdutils.register() From 8a107fa5b365b70600556bc9889076e629867f6e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Dec 2018 17:38:50 +0100 Subject: [PATCH 169/258] Fix int_level handling in :zoom --- qutebrowser/components/zoomcommands.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py index 69f1df1c4..7a392c4a0 100644 --- a/qutebrowser/components/zoomcommands.py +++ b/qutebrowser/components/zoomcommands.py @@ -82,10 +82,9 @@ def zoom(tab: apitypes.Tab, except ValueError: raise cmdutils.CommandError("zoom: Invalid int value {}" .format(level)) - - if count is not None: + elif count is not None: int_level = count - elif int_level is None: + else: int_level = config.val.zoom.default try: From 2770a935e9d5fefcdd72ca3787ac406bed13f0e1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 07:57:21 +0100 Subject: [PATCH 170/258] Properly fix zoom handling --- qutebrowser/components/zoomcommands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/components/zoomcommands.py b/qutebrowser/components/zoomcommands.py index 7a392c4a0..51d01cfea 100644 --- a/qutebrowser/components/zoomcommands.py +++ b/qutebrowser/components/zoomcommands.py @@ -76,16 +76,16 @@ def zoom(tab: apitypes.Tab, count: The zoom percentage to set. quiet: Don't show a zoom level message. """ - if level is not None: + if count is not None: + int_level = count + elif level is not None: try: int_level = int(level.rstrip('%')) except ValueError: raise cmdutils.CommandError("zoom: Invalid int value {}" .format(level)) - elif count is not None: - int_level = count else: - int_level = config.val.zoom.default + int_level = int(config.val.zoom.default) try: tab.zoom.set_factor(int_level / 100) From c752ba472184adc539bb22584367f2028b21e8d5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 08:55:35 +0100 Subject: [PATCH 171/258] Fix coverage --- qutebrowser/config/configfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 54ca91488..d11c0d976 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -38,7 +38,7 @@ from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch MYPY = False -if MYPY: +if MYPY: # pragma: no cover # pylint: disable=unused-import, useless-suppression from qutebrowser.misc import savemanager From 403e63d6f27d397be0b72c7def7e2541523bff78 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 09:05:32 +0100 Subject: [PATCH 172/258] Configure coverage.py for "if MYPY:" --- .coveragerc | 1 + qutebrowser/api/config.py | 2 +- qutebrowser/config/config.py | 2 +- qutebrowser/config/configcommands.py | 2 +- qutebrowser/config/configfiles.py | 2 +- qutebrowser/config/configutils.py | 2 +- qutebrowser/misc/objects.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9ba8e8a5e..69b126e12 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,7 @@ exclude_lines = raise NotImplementedError raise utils\.Unreachable if __name__ == ["']__main__["']: + if MYPY: [xml] output=coverage.xml diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index d45703242..6558cf42a 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -22,7 +22,7 @@ import typing MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.config import config diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 44bf3ca77..80826beeb 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -33,7 +33,7 @@ from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import,useless-suppression from typing import Tuple, MutableMapping from qutebrowser.config import configcache, configfiles diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 0ee77fcc9..5d5b2db7e 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -33,7 +33,7 @@ from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.config.config import Config, KeyConfig diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index d11c0d976..54ca91488 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -38,7 +38,7 @@ from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import, useless-suppression from qutebrowser.misc import savemanager diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 9d4dc94ef..efdc5611e 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -30,7 +30,7 @@ from qutebrowser.utils import utils, urlmatch from qutebrowser.config import configexc MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.config import configdata diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index ccb5d09b3..0bb26954c 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -25,7 +25,7 @@ import typing MYPY = False -if MYPY: # pragma: no cover +if MYPY: # pylint: disable=unused-import,useless-suppression from qutebrowser.utils import usertypes from qutebrowser.commands import command From f53fd56c3d7a4f7e9eff5b84707d8dee738f6515 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 16:48:33 +0100 Subject: [PATCH 173/258] mypy: Add annotations for qutebrowser.config.configtypes --- mypy.ini | 4 + qutebrowser/config/configexc.py | 3 +- qutebrowser/config/configtypes.py | 467 +++++++++++++++----------- qutebrowser/config/configutils.py | 4 +- tests/unit/config/test_configtypes.py | 2 +- tests/unit/config/test_configutils.py | 2 +- 6 files changed, 290 insertions(+), 192 deletions(-) diff --git a/mypy.ini b/mypy.ini index c61ca1f18..2e1d50558 100644 --- a/mypy.ini +++ b/mypy.ini @@ -98,6 +98,10 @@ disallow_incomplete_defs = True disallow_untyped_defs = True disallow_incomplete_defs = True +[mypy-qutebrowser.config.configtypes] +disallow_untyped_defs = True +disallow_incomplete_defs = True + [mypy-qutebrowser.api.*] disallow_untyped_defs = True disallow_incomplete_defs = True diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index b1dc04e09..80a2cedb2 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -76,7 +76,8 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value: typing.Any, msg: str) -> None: + def __init__(self, value: typing.Any, + msg: typing.Union[str, Exception]) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d4ce33d21..61dba365e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -52,13 +52,14 @@ import datetime import functools import operator import json -import typing # pylint: disable=unused-import +import typing import attr import yaml from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar +from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects from qutebrowser.config import configexc, configutils @@ -67,13 +68,23 @@ from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch, from qutebrowser.keyinput import keyutils -SYSTEM_PROXY = object() # Return value for Proxy type +class _SystemProxy: + + pass + + +SYSTEM_PROXY = _SystemProxy() # Return value for Proxy type # Taken from configparser BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} +_Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]] +_StrUnset = typing.Union[str, configutils.Unset] +_StrUnsetNone = typing.Union[None, str, configutils.Unset] + + class ValidValues: """Container for valid values for a given type. @@ -84,11 +95,15 @@ class ValidValues: generate_docs: Whether to show the values in the docs. """ - def __init__(self, *values, generate_docs=True): + def __init__(self, + *values: typing.Union[str, + typing.Dict[str, str], + typing.Tuple[str, str]], + generate_docs: bool = True) -> None: if not values: raise ValueError("ValidValues with no values makes no sense!") - self.descriptions = {} - self.values = [] + self.descriptions = {} # type: typing.Dict[str, str] + self.values = [] # type: typing.List[str] self.generate_docs = generate_docs for value in values: if isinstance(value, str): @@ -105,17 +120,18 @@ class ValidValues: self.values.append(value[0]) self.descriptions[value[0]] = value[1] - def __contains__(self, val): + def __contains__(self, val: str) -> bool: return val in self.values - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: return self.values.__iter__() - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, values=self.values, descriptions=self.descriptions) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + assert isinstance(other, ValidValues) return (self.values == other.values and self.descriptions == other.descriptions) @@ -132,26 +148,28 @@ class BaseType: string. ValidValues instance. """ - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: self.none_ok = none_ok - self.valid_values = None + self.valid_values = None # type: typing.Optional[ValidValues] - def get_name(self): + def get_name(self) -> str: """Get a name for the type for documentation.""" return self.__class__.__name__ - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: """Get the type's valid values for documentation.""" return self.valid_values - def _basic_py_validation(self, value, pytype): + def _basic_py_validation( + self, value: typing.Any, + pytype: typing.Union[type, typing.Tuple[type, ...]]) -> None: """Do some basic validation for Python values (emptyness, type). Arguments: value: The value to check. pytype: A Python type to check the value against. """ - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return if (value is None or (pytype == list and value == []) or @@ -174,7 +192,7 @@ class BaseType: if isinstance(value, str): self._basic_str_validation(value) - def _basic_str_validation(self, value): + def _basic_str_validation(self, value: str) -> None: """Do some basic validation for string values. This checks that the value isn't empty and doesn't contain any @@ -190,7 +208,8 @@ class BaseType: raise configexc.ValidationError( value, "may not contain unprintable chars!") - def _validate_surrogate_escapes(self, full_value, value): + def _validate_surrogate_escapes(self, full_value: typing.Any, + value: typing.Any) -> None: """Make sure the given value doesn't contain surrogate escapes. This is used for values passed to json.dump, as it can't handle those. @@ -201,7 +220,7 @@ class BaseType: raise configexc.ValidationError( full_value, "may not contain surrogate escapes!") - def _validate_valid_values(self, value): + def _validate_valid_values(self, value: str) -> None: """Validate value against possible values. The default implementation checks the value against self.valid_values @@ -216,7 +235,7 @@ class BaseType: value, "valid values: {}".format(', '.join(self.valid_values))) - def from_str(self, value): + def from_str(self, value: str) -> typing.Any: """Get the setting value from a string. By default this invokes to_py() for validation and returns the @@ -235,11 +254,11 @@ class BaseType: return None return value - def from_obj(self, value): + def from_obj(self, value: typing.Any) -> typing.Any: """Get the setting value from a config.py/YAML object.""" return value - def to_py(self, value): + def to_py(self, value: typing.Any) -> typing.Any: """Get the setting value from a Python value. Args: @@ -253,7 +272,7 @@ class BaseType: """ raise NotImplementedError - def to_str(self, value): + def to_str(self, value: typing.Any) -> str: """Get a string from the setting value. The resulting string should be parseable again by from_str. @@ -263,7 +282,7 @@ class BaseType: assert isinstance(value, str), value return value - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.Any, indent: int = 0) -> str: """Get a string with the given value for the documentation. This currently uses asciidoc syntax. @@ -274,7 +293,7 @@ class BaseType: return 'empty' return '+pass:[{}]+'.format(html.escape(str_value)) - def complete(self): + def complete(self) -> _Completions: """Return a list of possible values for completion. The default implementation just returns valid_values, but it might be @@ -308,13 +327,14 @@ class MappingType(BaseType): MAPPING = {} # type: typing.Dict[str, typing.Any] - def __init__(self, none_ok=False, valid_values=None): + def __init__(self, none_ok: bool = False, + valid_values: ValidValues = None) -> None: super().__init__(none_ok) self.valid_values = valid_values - def to_py(self, value): + def to_py(self, value: typing.Any) -> typing.Any: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -335,9 +355,11 @@ class String(BaseType): completions: completions to be used, or None """ - def __init__(self, *, minlen=None, maxlen=None, forbidden=None, - encoding=None, none_ok=False, completions=None, - valid_values=None): + def __init__(self, *, minlen: int = None, maxlen: int = None, + forbidden: str = None, encoding: str = None, + none_ok: bool = False, + completions: _Completions = None, + valid_values: ValidValues = None) -> None: super().__init__(none_ok) self.valid_values = valid_values @@ -354,7 +376,7 @@ class String(BaseType): self._completions = completions self.encoding = encoding - def _validate_encoding(self, value): + def _validate_encoding(self, value: str) -> None: """Check if the given value fits into the configured encoding. Raises ValidationError if not. @@ -372,9 +394,9 @@ class String(BaseType): value, self.encoding, e) raise configexc.ValidationError(value, msg) - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -395,7 +417,7 @@ class String(BaseType): return value - def complete(self): + def complete(self) -> _Completions: if self._completions is not None: return self._completions else: @@ -406,19 +428,19 @@ class UniqueCharString(String): """A string which may not contain duplicate chars.""" - def to_py(self, value): - value = super().to_py(value) - if value is configutils.UNSET: - return value - elif not value: + def to_py(self, value: _StrUnset) -> _StrUnsetNone: + py_value = super().to_py(value) + if isinstance(py_value, configutils.Unset): + return py_value + elif not py_value: return None # Check for duplicate values - if len(set(value)) != len(value): + if len(set(py_value)) != len(py_value): raise configexc.ValidationError( - value, "String contains duplicate values!") + py_value, "String contains duplicate values!") - return value + return py_value class List(BaseType): @@ -430,21 +452,23 @@ class List(BaseType): _show_valtype = True - def __init__(self, valtype, none_ok=False, length=None): + def __init__(self, valtype: BaseType, + none_ok: bool = False, + length: int = None) -> None: super().__init__(none_ok) self.valtype = valtype self.length = length - def get_name(self): + def get_name(self) -> str: name = super().get_name() if self._show_valtype: name += " of " + self.valtype.get_name() return name - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[typing.List]: self._basic_str_validation(value) if not value: return None @@ -459,14 +483,17 @@ class List(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value): + def from_obj(self, value: typing.Optional[typing.List]) -> typing.List: if value is None: return [] return [self.valtype.from_obj(v) for v in value] - def to_py(self, value): + def to_py( + self, + value: typing.Union[typing.List, configutils.Unset] + ) -> typing.Union[typing.List, configutils.Unset]: self._basic_py_validation(value, list) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return [] @@ -479,13 +506,13 @@ class List(BaseType): "be set!".format(self.length)) return [self.valtype.to_py(v) for v in value] - def to_str(self, value): + def to_str(self, value: typing.List) -> str: if not value: # An empty list is treated just like None -> empty string return '' return json.dumps(value) - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.List, indent: int = 0) -> str: if not value: return 'empty' @@ -513,13 +540,16 @@ class ListOrValue(BaseType): _show_valtype = True - def __init__(self, valtype, *args, none_ok=False, **kwargs): + def __init__(self, valtype: BaseType, *, + none_ok: bool = False, + **kwargs: typing.Any) -> None: super().__init__(none_ok) assert not isinstance(valtype, (List, ListOrValue)), valtype - self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs) + self.listtype = List(valtype, none_ok=none_ok, **kwargs) self.valtype = valtype - def _val_and_type(self, value): + def _val_and_type(self, + value: typing.Any) -> typing.Tuple[typing.Any, BaseType]: """Get the value and type to use for to_str/to_doc/from_str.""" if isinstance(value, list): if len(value) == 1: @@ -529,25 +559,25 @@ class ListOrValue(BaseType): else: return value, self.valtype - def get_name(self): + def get_name(self) -> str: return self.listtype.get_name() + ', or ' + self.valtype.get_name() - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value): + def from_str(self, value: str) -> typing.Any: try: return self.listtype.from_str(value) except configexc.ValidationError: return self.valtype.from_str(value) - def from_obj(self, value): + def from_obj(self, value: typing.Any) -> typing.Any: if value is None: return [] return value - def to_py(self, value): - if value is configutils.UNSET: + def to_py(self, value: typing.Any) -> typing.Any: + if isinstance(value, configutils.Unset): return value try: @@ -555,14 +585,14 @@ class ListOrValue(BaseType): except configexc.ValidationError: return self.listtype.to_py(value) - def to_str(self, value): + def to_str(self, value: typing.Any) -> str: if value is None: return '' val, typ = self._val_and_type(value) return typ.to_str(val) - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.Any, indent: int = 0) -> str: if value is None: return 'empty' @@ -578,26 +608,31 @@ class FlagList(List): the valid values of the setting. """ - combinable_values = None # type: typing.Optional[typing.Iterable] + combinable_values = None # type: typing.Optional[typing.Sequence] _show_valtype = False - def __init__(self, none_ok=False, valid_values=None, length=None): + def __init__(self, none_ok: bool = False, + valid_values: ValidValues = None, + length: int = None) -> None: super().__init__(valtype=String(), none_ok=none_ok, length=length) self.valtype.valid_values = valid_values - def _check_duplicates(self, values): + def _check_duplicates(self, values: typing.List) -> None: if len(set(values)) != len(values): raise configexc.ValidationError( values, "List contains duplicate values!") - def to_py(self, value): + def to_py( + self, + value: typing.Union[configutils.Unset, typing.List], + ) -> typing.Union[configutils.Unset, typing.List]: vals = super().to_py(value) - if vals is not configutils.UNSET: + if not isinstance(vals, configutils.Unset): self._check_duplicates(vals) return vals - def complete(self): + def complete(self) -> _Completions: valid_values = self.valtype.valid_values if valid_values is None: return None @@ -626,15 +661,15 @@ class Bool(BaseType): while `0`, `no`, `off` and `false` count as false (case-insensitive). """ - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', generate_docs=False) - def to_py(self, value): + def to_py(self, value: typing.Optional[bool]) -> typing.Optional[bool]: self._basic_py_validation(value, bool) return value - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[bool]: self._basic_str_validation(value) if not value: return None @@ -644,7 +679,7 @@ class Bool(BaseType): except KeyError: raise configexc.ValidationError(value, "must be a boolean!") - def to_str(self, value): + def to_str(self, value: typing.Optional[bool]) -> str: mapping = { None: '', True: 'true', @@ -657,25 +692,27 @@ class BoolAsk(Bool): """Like `Bool`, but `ask` is allowed as additional value.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', 'ask') - def to_py(self, value): + def to_py(self, # type: ignore + value: typing.Union[bool, str]) -> typing.Union[bool, str, None]: # basic validation unneeded if it's == 'ask' and done by Bool if we # call super().to_py if isinstance(value, str) and value.lower() == 'ask': return 'ask' - return super().to_py(value) + return super().to_py(value) # type: ignore - def from_str(self, value): + def from_str(self, # type: ignore + value: str) -> typing.Union[bool, str, None]: # basic validation unneeded if it's == 'ask' and done by Bool if we # call super().from_str if isinstance(value, str) and value.lower() == 'ask': return 'ask' return super().from_str(value) - def to_str(self, value): + def to_str(self, value: typing.Union[bool, str, None]) -> str: mapping = { None: '', True: 'true', @@ -694,7 +731,9 @@ class _Numeric(BaseType): # pylint: disable=abstract-method maxval: Maximum value (inclusive). """ - def __init__(self, minval=None, maxval=None, none_ok=False): + def __init__(self, minval: int = None, + maxval: int = None, + none_ok: bool = False) -> None: super().__init__(none_ok) self.minval = self._parse_bound(minval) self.maxval = self._parse_bound(maxval) @@ -703,7 +742,9 @@ class _Numeric(BaseType): # pylint: disable=abstract-method raise ValueError("minval ({}) needs to be <= maxval ({})!" .format(self.minval, self.maxval)) - def _parse_bound(self, bound): + def _parse_bound( + self, bound: typing.Union[None, str, int, float] + ) -> typing.Union[None, int, float]: """Get a numeric bound from a string like 'maxint'.""" if bound == 'maxint': return qtutils.MAXVALS['int'] @@ -714,7 +755,8 @@ class _Numeric(BaseType): # pylint: disable=abstract-method assert isinstance(bound, (int, float)), bound return bound - def _validate_bounds(self, value, suffix=''): + def _validate_bounds(self, value: typing.Union[None, int, float], + suffix: str = '') -> None: """Validate self.minval and self.maxval.""" if value is None: return @@ -725,7 +767,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method raise configexc.ValidationError( value, "must be {}{} or smaller!".format(self.maxval, suffix)) - def to_str(self, value): + def to_str(self, value: typing.Union[None, int, float]) -> str: if value is None: return '' return str(value) @@ -735,7 +777,7 @@ class Int(_Numeric): """Base class for an integer setting.""" - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[int]: self._basic_str_validation(value) if not value: return None @@ -747,7 +789,7 @@ class Int(_Numeric): self.to_py(intval) return intval - def to_py(self, value): + def to_py(self, value: typing.Optional[int]) -> typing.Optional[int]: self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -757,7 +799,7 @@ class Float(_Numeric): """Base class for a float setting.""" - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[float]: self._basic_str_validation(value) if not value: return None @@ -769,7 +811,10 @@ class Float(_Numeric): self.to_py(floatval) return floatval - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, int, float], + ) -> typing.Union[None, int, float]: self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value @@ -779,9 +824,12 @@ class Perc(_Numeric): """A percentage.""" - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, float, int, str, configutils.Unset] + ) -> typing.Union[None, float, int, configutils.Unset]: self._basic_py_validation(value, (float, int, str)) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -796,7 +844,7 @@ class Perc(_Numeric): self._validate_bounds(value, suffix='%') return value - def to_str(self, value): + def to_str(self, value: typing.Union[None, float, int, str]) -> str: if value is None: return '' return value @@ -813,8 +861,9 @@ class PercOrInt(_Numeric): maxint: Maximum value for integer (inclusive). """ - def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None, - none_ok=False): + def __init__(self, minperc: int = None, maxperc: int = None, + minint: int = None, maxint: int = None, + none_ok: bool = False) -> None: super().__init__(minval=minint, maxval=maxint, none_ok=none_ok) self.minperc = self._parse_bound(minperc) self.maxperc = self._parse_bound(maxperc) @@ -823,7 +872,7 @@ class PercOrInt(_Numeric): raise ValueError("minperc ({}) needs to be <= maxperc " "({})!".format(self.minperc, self.maxperc)) - def from_str(self, value): + def from_str(self, value: str) -> typing.Union[None, str, int]: self._basic_str_validation(value) if not value: return None @@ -840,7 +889,10 @@ class PercOrInt(_Numeric): self.to_py(intval) return intval - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, str, int] + ) -> typing.Union[None, str, int]: """Expect a value like '42%' as string, or 23 as int.""" self._basic_py_validation(value, (int, str)) if value is None: @@ -880,13 +932,13 @@ class Command(BaseType): invalid commands (in bindings/aliases) fail when used. """ - def complete(self): + def complete(self) -> _Completions: out = [] for cmdname, obj in objects.commands.items(): out.append((cmdname, obj.desc)) return out - def to_py(self, value): + def to_py(self, value: str) -> str: self._basic_py_validation(value, str) return value @@ -895,7 +947,7 @@ class ColorSystem(MappingType): """The color system to use for color interpolation.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -916,7 +968,7 @@ class IgnoreCase(MappingType): """Whether to search case insensitively.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -946,7 +998,7 @@ class QtColor(BaseType): * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) """ - def _parse_value(self, val): + def _parse_value(self, val: str) -> int: try: return int(val) except ValueError: @@ -962,9 +1014,10 @@ class QtColor(BaseType): except ValueError: raise configexc.ValidationError(val, "must be a valid color value") - def to_py(self, value): + def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + None, QColor]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -973,15 +1026,15 @@ class QtColor(BaseType): openparen = value.index('(') kind = value[:openparen] vals = value[openparen+1:-1].split(',') - vals = [self._parse_value(v) for v in vals] - if kind == 'rgba' and len(vals) == 4: - return QColor.fromRgb(*vals) - elif kind == 'rgb' and len(vals) == 3: - return QColor.fromRgb(*vals) - elif kind == 'hsva' and len(vals) == 4: - return QColor.fromHsv(*vals) - elif kind == 'hsv' and len(vals) == 3: - return QColor.fromHsv(*vals) + int_vals = [self._parse_value(v) for v in vals] + if kind == 'rgba' and len(int_vals) == 4: + return QColor.fromRgb(*int_vals) + elif kind == 'rgb' and len(int_vals) == 3: + return QColor.fromRgb(*int_vals) + elif kind == 'hsva' and len(int_vals) == 4: + return QColor.fromHsv(*int_vals) + elif kind == 'hsv' and len(int_vals) == 3: + return QColor.fromHsv(*int_vals) else: raise configexc.ValidationError(value, "must be a valid color") @@ -1009,9 +1062,9 @@ class QssColor(BaseType): under ``Gradient'' """ - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1056,9 +1109,9 @@ class Font(BaseType): )* # 0-inf size/weight/style tags (?P.+) # mandatory font family""", re.VERBOSE) - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1077,9 +1130,9 @@ class FontFamily(Font): """A Qt font family.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1103,9 +1156,10 @@ class QtFont(Font): __doc__ = Font.__doc__ # for src2asciidoc.py - def to_py(self, value): + def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + None, QFont]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1176,18 +1230,19 @@ class Regex(BaseType): _regex_type: The Python type of a regex object. """ - def __init__(self, flags=0, none_ok=False): + def __init__(self, flags: str = None, + none_ok: bool = False) -> None: super().__init__(none_ok) self._regex_type = type(re.compile('')) # Parse flags from configdata.yml - if flags == 0: - self.flags = flags + if flags is None: + self.flags = 0 else: self.flags = functools.reduce( operator.or_, (getattr(re, flag.strip()) for flag in flags.split(' | '))) - def _compile_regex(self, pattern): + def _compile_regex(self, pattern: str) -> typing.Pattern[str]: """Check if the given regex is valid. This is more complicated than it could be since there's a warning on @@ -1206,6 +1261,8 @@ class Regex(BaseType): pattern, "must be a valid regex - recursion depth " "exceeded") + assert recorded_warnings is not None + for w in recorded_warnings: if (issubclass(w.category, DeprecationWarning) and str(w.message).startswith('bad escape')): @@ -1216,10 +1273,13 @@ class Regex(BaseType): return compiled - def to_py(self, value): + def to_py( + self, + value: typing.Union[str, typing.Pattern[str]] + ) -> typing.Union[configutils.Unset, None, typing.Pattern[str]]: """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1228,12 +1288,14 @@ class Regex(BaseType): else: return value - def to_str(self, value): + def to_str(self, + value: typing.Union[None, str, typing.Pattern[str]]) -> str: if value is None: return '' elif isinstance(value, self._regex_type): return value.pattern else: + assert isinstance(value, str) return value @@ -1244,8 +1306,11 @@ class Dict(BaseType): When setting from a string, pass a json-like dict, e.g. `{"key", "value"}`. """ - def __init__(self, keytype, valtype, *, fixed_keys=None, - required_keys=None, none_ok=False): + def __init__(self, keytype: typing.Union[String, 'Key'], + valtype: BaseType, *, + fixed_keys: typing.Iterable = None, + required_keys: typing.Iterable = None, + none_ok: bool = False) -> None: super().__init__(none_ok) # If the keytype is not a string, we'll get problems with showing it as # json in to_str() as json converts keys to strings. @@ -1255,7 +1320,7 @@ class Dict(BaseType): self.fixed_keys = fixed_keys self.required_keys = required_keys - def _validate_keys(self, value): + def _validate_keys(self, value: typing.Dict) -> None: if (self.fixed_keys is not None and not set(value.keys()).issubset(self.fixed_keys)): raise configexc.ValidationError( @@ -1266,7 +1331,7 @@ class Dict(BaseType): raise configexc.ValidationError( value, "Required keys {}".format(self.required_keys)) - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[typing.Dict]: self._basic_str_validation(value) if not value: return None @@ -1281,14 +1346,14 @@ class Dict(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value): + def from_obj(self, value: typing.Optional[typing.Dict]) -> typing.Dict: if value is None: return {} return {self.keytype.from_obj(key): self.valtype.from_obj(val) for key, val in value.items()} - def _fill_fixed_keys(self, value): + def _fill_fixed_keys(self, value: typing.Dict) -> typing.Dict: """Fill missing fixed keys with a None-value.""" if self.fixed_keys is None: return value @@ -1297,9 +1362,12 @@ class Dict(BaseType): value[key] = self.valtype.to_py(None) return value - def to_py(self, value): + def to_py( + self, + value: typing.Union[typing.Dict, configutils.Unset, None] + ) -> typing.Union[typing.Dict, configutils.Unset]: self._basic_py_validation(value, dict) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return self._fill_fixed_keys({}) @@ -1313,13 +1381,13 @@ class Dict(BaseType): for key, val in value.items()} return self._fill_fixed_keys(d) - def to_str(self, value): + def to_str(self, value: typing.Dict) -> str: if not value: # An empty Dict is treated just like None -> empty string return '' return json.dumps(value, sort_keys=True) - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.Dict, indent: int = 0) -> str: if not value: return 'empty' lines = ['\n'] @@ -1337,13 +1405,13 @@ class File(BaseType): """A file on the local filesystem.""" - def __init__(self, required=True, **kwargs): + def __init__(self, required: bool = True, **kwargs: typing.Any) -> None: super().__init__(**kwargs) self.required = required - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1369,9 +1437,9 @@ class Directory(BaseType): """A directory on the local filesystem.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1394,13 +1462,14 @@ class FormatString(BaseType): """A string with placeholders.""" - def __init__(self, fields, none_ok=False): + def __init__(self, fields: typing.Iterable[str], + none_ok: bool = False) -> None: super().__init__(none_ok) self.fields = fields - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1428,39 +1497,46 @@ class ShellCommand(List): _show_valtype = False - def __init__(self, placeholder=False, none_ok=False): + def __init__(self, placeholder: bool = False, + none_ok: bool = False) -> None: super().__init__(valtype=String(), none_ok=none_ok) self.placeholder = placeholder - def to_py(self, value): - value = super().to_py(value) - if value is configutils.UNSET: - return value - elif not value: + def to_py( + self, + value: typing.Union[typing.List, configutils.Unset], + ) -> typing.Union[typing.List, configutils.Unset]: + py_value = super().to_py(value) + if isinstance(py_value, configutils.Unset): + return py_value + elif not py_value: return [] if (self.placeholder and - '{}' not in ' '.join(value) and - '{file}' not in ' '.join(value)): - raise configexc.ValidationError(value, "needs to contain a " + '{}' not in ' '.join(py_value) and + '{file}' not in ' '.join(py_value)): + raise configexc.ValidationError(py_value, "needs to contain a " "{}-placeholder or a " "{file}-placeholder.") - return value + return py_value class Proxy(BaseType): """A proxy URL, or `system`/`none`.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues( ('system', "Use the system wide proxy."), ('none', "Don't use any proxy")) - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, QNetworkProxy, _SystemProxy]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1474,13 +1550,15 @@ class Proxy(BaseType): else: # If we add a special value to valid_values, we need to handle # it here! + assert self.valid_values is not None assert value not in self.valid_values, value url = QUrl(value) return urlutils.proxy_from_url(url) except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e: raise configexc.ValidationError(value, e) - def complete(self): + def complete(self) -> _Completions: + assert self.valid_values is not None out = [] for val in self.valid_values: out.append((val, self.valid_values.descriptions[val])) @@ -1496,9 +1574,9 @@ class SearchEngineUrl(BaseType): """A search engine URL.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1526,9 +1604,9 @@ class FuzzyUrl(BaseType): """A URL which gets interpreted as search if needed.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1544,10 +1622,10 @@ class PaddingValues: """Four padding values.""" - top = attr.ib() - bottom = attr.ib() - left = attr.ib() - right = attr.ib() + top = attr.ib() # type: int + bottom = attr.ib() # type: int + left = attr.ib() # type: int + right = attr.ib() # type: int class Padding(Dict): @@ -1556,15 +1634,18 @@ class Padding(Dict): _show_valtype = False - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(keytype=String(), valtype=Int(minval=0, none_ok=none_ok), fixed_keys=['top', 'bottom', 'left', 'right'], none_ok=none_ok) - def to_py(self, value): + def to_py( # type: ignore + self, + value: typing.Union[configutils.Unset, typing.Dict, None], + ) -> typing.Union[configutils.Unset, PaddingValues]: d = super().to_py(value) - if d is configutils.UNSET: + if isinstance(d, configutils.Unset): return d return PaddingValues(**d) @@ -1574,9 +1655,9 @@ class Encoding(BaseType): """Setting for a python encoding.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1598,7 +1679,7 @@ class Position(MappingType): 'right': QTabWidget.East, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues('top', 'bottom', 'left', 'right')) @@ -1614,7 +1695,7 @@ class TextAlignment(MappingType): 'center': Qt.AlignCenter, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues('left', 'right', 'center')) @@ -1624,7 +1705,7 @@ class VerticalPosition(String): """The position of the download bar.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) self.valid_values = ValidValues('top', 'bottom') @@ -1633,9 +1714,12 @@ class Url(BaseType): """A URL as a string.""" - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, QUrl]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1651,9 +1735,9 @@ class SessionName(BaseType): """The name of a session.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1672,7 +1756,7 @@ class SelectOnRemove(MappingType): 'last-used': QTabBar.SelectPreviousTab, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -1690,7 +1774,7 @@ class ConfirmQuit(FlagList): # Values that can be combined with commas combinable_values = ('multiple-tabs', 'downloads') - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valtype.none_ok = none_ok self.valtype.valid_values = ValidValues( @@ -1701,9 +1785,12 @@ class ConfirmQuit(FlagList): "downloads are running"), ('never', "Never show a confirmation.")) - def to_py(self, value): + def to_py( + self, + value: typing.Union[configutils.Unset, typing.List], + ) -> typing.Union[typing.List, configutils.Unset]: values = super().to_py(value) - if values is configutils.UNSET: + if isinstance(values, configutils.Unset): return values elif not values: return [] @@ -1724,7 +1811,7 @@ class NewTabPosition(String): """How new tabs are positioned.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) self.valid_values = ValidValues( ('prev', "Before the current tab."), @@ -1740,9 +1827,9 @@ class TimestampTemplate(BaseType): See https://sqlite.org/lang_datefunc.html for reference. """ - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1762,13 +1849,16 @@ class Key(BaseType): """A name of a key.""" - def from_obj(self, value): + def from_obj(self, value: str) -> str: """Make sure key sequences are always normalized.""" return str(keyutils.KeySequence.parse(value)) - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, keyutils.KeySequence]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1787,9 +1877,12 @@ class UrlPattern(BaseType): syntax. """ - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, urlmatch.UrlPattern]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index efdc5611e..47cac4bff 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -35,7 +35,7 @@ if MYPY: from qutebrowser.config import configdata -class _UnsetObject: +class Unset: """Sentinel object.""" @@ -45,7 +45,7 @@ class _UnsetObject: return '' -UNSET = _UnsetObject() +UNSET = Unset() @attr.s diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index a9a61e952..76bdb9199 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1542,7 +1542,7 @@ class TestRegex: regex.to_py('foo') @pytest.mark.parametrize('flags, expected', [ - (0, 0), + (None, 0), ('IGNORECASE', re.IGNORECASE), ('IGNORECASE | VERBOSE', re.IGNORECASE | re.VERBOSE), ]) diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py index 587a0bd68..e8a7bfb38 100644 --- a/tests/unit/config/test_configutils.py +++ b/tests/unit/config/test_configutils.py @@ -26,7 +26,7 @@ from qutebrowser.utils import urlmatch def test_unset_object_identity(): - assert configutils._UnsetObject() is not configutils._UnsetObject() + assert configutils.Unset() is not configutils.Unset() assert configutils.UNSET is configutils.UNSET From a9c1fc665fa54c28bfd6dfc51df0ce7849d382a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 16:57:09 +0100 Subject: [PATCH 174/258] Fix configtypes.Perc.to_str() If we used an int/float in config.py for a Perc value (e.g. zoom.default), to_str() returned int/float instead of str, causing qWarnings and bugs. --- qutebrowser/config/configtypes.py | 5 ++++- tests/unit/config/test_configtypes.py | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 61dba365e..3f134f770 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -847,7 +847,10 @@ class Perc(_Numeric): def to_str(self, value: typing.Union[None, float, int, str]) -> str: if value is None: return '' - return value + elif isinstance(value, str): + return value + else: + return '{}%'.format(value) class PercOrInt(_Numeric): diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 76bdb9199..ef41e5d75 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1113,8 +1113,13 @@ class TestPerc: with pytest.raises(configexc.ValidationError): klass(**kwargs).to_py(val) - def test_to_str(self, klass): - assert klass().to_str('42%') == '42%' + @pytest.mark.parametrize('value, expected', [ + ('42%', '42%'), + (42, '42%'), + (42.5, '42.5%'), + ]) + def test_to_str(self, klass, value, expected): + assert klass().to_str(value) == expected class TestPercOrInt: From 7bddb7867bb88a791da356bc6c81faa95994017b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 17:23:09 +0100 Subject: [PATCH 175/258] Add missing test for configtypes.Key --- tests/unit/config/test_configtypes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index ef41e5d75..b0b85d997 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -2123,6 +2123,9 @@ class TestKey: with pytest.raises(configexc.ValidationError): klass().to_py(val) + def test_normalized(self, klass): + assert klass().from_obj('') == '' + class TestUrlPattern: From b9cb98a5ef282497fc11e70ad428e056b7886d12 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 17:48:56 +0100 Subject: [PATCH 176/258] mypy: Add types for config.websettings --- mypy.ini | 38 +------------------------------ qutebrowser/config/websettings.py | 31 +++++++++++++------------ 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/mypy.ini b/mypy.ini index 2e1d50558..4526e4e48 100644 --- a/mypy.ini +++ b/mypy.ini @@ -62,43 +62,7 @@ disallow_incomplete_defs = True disallow_untyped_defs = True disallow_incomplete_defs = True -[mypy-qutebrowser.config.config] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configcache] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configcommands] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configdata] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configdiff] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configexc] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configfiles] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configinit] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configutils] -disallow_untyped_defs = True -disallow_incomplete_defs = True - -[mypy-qutebrowser.config.configtypes] +[mypy-qutebrowser.config.*] disallow_untyped_defs = True disallow_incomplete_defs = True diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index e6d19db7e..5a21af7e3 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -19,8 +19,10 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" -import typing # pylint: disable=unused-import +import typing +import argparse +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QFont from qutebrowser.config import config, configutils @@ -34,7 +36,8 @@ class AttributeInfo: """Info about a settings attribute.""" - def __init__(self, *attributes, converter=None): + def __init__(self, *attributes: typing.Any, + converter: typing.Callable = None) -> None: self.attributes = attributes if converter is None: self.converter = lambda val: val @@ -51,10 +54,10 @@ class AbstractSettings: _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any] _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint] - def __init__(self, settings): + def __init__(self, settings: typing.Any) -> None: self._settings = settings - def set_attribute(self, name, value): + def set_attribute(self, name: str, value: typing.Any) -> bool: """Set the given QWebSettings/QWebEngineSettings attribute. If the value is configutils.UNSET, the value is reset instead. @@ -75,7 +78,7 @@ class AbstractSettings: return old_value != new_value - def test_attribute(self, name): + def test_attribute(self, name: str) -> bool: """Get the value for the given attribute. If the setting resolves to a list of attributes, only the first @@ -84,7 +87,7 @@ class AbstractSettings: info = self._ATTRIBUTES[name] return self._settings.testAttribute(info.attributes[0]) - def set_font_size(self, name, value): + def set_font_size(self, name: str, value: int) -> bool: """Set the given QWebSettings/QWebEngineSettings font size. Return: @@ -96,7 +99,7 @@ class AbstractSettings: self._settings.setFontSize(family, value) return old_value != value - def set_font_family(self, name, value): + def set_font_family(self, name: str, value: typing.Optional[str]) -> bool: """Set the given QWebSettings/QWebEngineSettings font family. With None (the default), QFont is used to get the default font for the @@ -117,7 +120,7 @@ class AbstractSettings: return value != old_value - def set_default_text_encoding(self, encoding): + def set_default_text_encoding(self, encoding: str) -> bool: """Set the default text encoding to use. Return: @@ -128,7 +131,7 @@ class AbstractSettings: self._settings.setDefaultTextEncoding(encoding) return old_value != encoding - def _update_setting(self, setting, value): + def _update_setting(self, setting: str, value: typing.Any) -> bool: """Update the given setting/value. Unknown settings are ignored. @@ -146,12 +149,12 @@ class AbstractSettings: return self.set_default_text_encoding(value) return False - def update_setting(self, setting): + def update_setting(self, setting: str) -> None: """Update the given setting.""" value = config.instance.get(setting) self._update_setting(setting, value) - def update_for_url(self, url): + def update_for_url(self, url: QUrl) -> typing.Set[str]: """Update settings customized for the given tab. Return: @@ -173,14 +176,14 @@ class AbstractSettings: return changed_settings - def init_settings(self): + def init_settings(self) -> None: """Set all supported settings correctly.""" for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) + list(self._FONT_FAMILIES)): self.update_setting(setting) -def init(args): +def init(args: argparse.Namespace) -> None: """Initialize all QWeb(Engine)Settings.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings @@ -195,7 +198,7 @@ def init(args): pattern=urlmatch.UrlPattern(pattern)) -def shutdown(): +def shutdown() -> None: """Shut down QWeb(Engine)Settings.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings From fe75ee146904c6f8c1020eb40b39734a5d8cadd2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Dec 2018 18:28:34 +0100 Subject: [PATCH 177/258] Mark "Using JS after window.open" as flaky --- tests/end2end/features/javascript.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 9f0d2f14b..e4b477e50 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -188,6 +188,7 @@ Feature: Javascript stuff And I open 500 without waiting Then "Showing error page for* 500" should be logged + @flaky Scenario: Using JS after window.open When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to true From d505ed9fa2c3b3bb056a400055a07765aefd57d4 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Fri, 7 Dec 2018 12:25:47 -0200 Subject: [PATCH 178/258] Use POSIX shell instead of bash on scripts/open_url_in_instance.sh --- scripts/open_url_in_instance.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/open_url_in_instance.sh b/scripts/open_url_in_instance.sh index a6ce0ed91..ec2a5a26d 100755 --- a/scripts/open_url_in_instance.sh +++ b/scripts/open_url_in_instance.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # initial idea: Florian Bruhin (The-Compiler) # author: Thore Bödecker (foxxx0) From 735e6123cde46691d2d3d42cda8d8ec4618d9760 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Dec 2018 12:34:59 +0100 Subject: [PATCH 179/258] 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 180/258] 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 181/258] 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 182/258] 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 183/258] 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 184/258] 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 185/258] 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 186/258] 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 187/258] 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 188/258] 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 189/258] 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 190/258] 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 191/258] 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 192/258] 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 193/258] 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 194/258] 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 195/258] 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 196/258] 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 197/258] 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 198/258] 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 199/258] 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 200/258] 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 201/258] 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 202/258] 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 203/258] 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 204/258] 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 205/258] 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 206/258] 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 207/258] 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 208/258] 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 209/258] 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 210/258] 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 211/258] 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 212/258] 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 213/258] 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 214/258] 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 215/258] 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 216/258] 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 217/258] 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 218/258] 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 219/258] 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 220/258] 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 221/258] 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 222/258] 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 223/258] 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 224/258] 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 225/258] 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 226/258] 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 227/258] 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 228/258] 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 229/258] 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 230/258] 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 231/258] 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 232/258] 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 233/258] 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 234/258] 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 235/258] 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 236/258] 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 237/258] 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 238/258] 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 239/258] 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 240/258] 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 241/258] 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 242/258] 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 243/258] 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 244/258] 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 245/258] 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 246/258] 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 247/258] 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 248/258] 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 249/258] 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 250/258] 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 251/258] 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 252/258] 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 253/258] 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 254/258] 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 255/258] 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 256/258] 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 257/258] 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 258/258] 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