diff --git a/.travis.yml b/.travis.yml
index 50f431626..017228385 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -43,8 +43,9 @@ matrix:
packages:
- libxkbcommon-x11-0
- ### PyQt 5.13 (Python 3.7, with coverage)
- - env: TESTENV=py37-pyqt513-cov
+ ### PyQt 5.13 (Python 3.8, with coverage)
+ - env: TESTENV=py38-pyqt513-cov
+ python: 3.8-dev
# http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a
addons:
apt:
diff --git a/README.asciidoc b/README.asciidoc
index 607108e96..dac18adc2 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -143,7 +143,13 @@ If you want to give me a beer or a pizza back, I'm trying to make it as easy as
possible for you to do so. If some other way would be easier for you, please
get in touch!
-* SEPA bank transfer inside Europe (no fee): Contact me at mail@qutebrowser.org for details
+* SEPA bank transfer inside Europe (no fee):
+ - Account holder: Florian Bruhin
+ - Country: Switzerland
+ - IBAN (EUR): CH13 0900 0000 9160 4094 6
+ - IBAN (other): CH80 0900 0000 8711 8587 3
+ - Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX)
+ - If you need any other information: Contact me at mail@qutebrowser.org.
* PayPal: https://www.paypal.me/thecompiler[thecompiler] / me@the-compiler.org
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 607eb4df8..5cff4577e 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -18,10 +18,32 @@ breaking changes (such as renamed commands) can happen in minor releases.
v1.9.0 (unreleased)
-------------------
+Changed
+~~~~~~~
+
+- The `qute-pass` userscript now has a new `--extra-url-suffixes` (`-s`)
+ argument which passes extra URL suffixes to the tldextract library.
+
Fixed
~~~~~
- dictcli.py now works correctly on Windows again.
+- Unbinding keys via `config.bind(key, None)` accidentally worked in
+ v1.7.0 but raises an exception in v1.8.0. It now works again, but is
+ deprecated and shows an error. Note that `:config-py-write` did write
+ such invalid lines before v1.8.0, so existing config files might need
+ adjustments.
+- The `readability-js` userscript now handles encodings correctly (which it
+ didn't before for some websites).
+- New `tabs.tooltips` setting which can be used to disable hover tooltips for
+ tabs.
+- can now be used to paste text starting with a hyphen.
+- Following hints via the number keypad now works properly again.
+- The logic for `:restart` has been revisited which should fix issues with
+ relative basedirs.
+- Errors while reading the state file are now displayed instead of causing a
+ crash.
+- Crash when using `:debug-log-level` without a console attached.
v1.8.1 (2019-09-27)
-------------------
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index d2a4d9328..51e600578 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -183,17 +183,21 @@ tox -e py35-cov -- tests/unit/browser/test_webelem.py
Profiling
~~~~~~~~~
-In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code
-and shows a graphical representation of what takes how much time.
+In the _scripts/dev/_ subfolder there's `run_profile.py` which profiles the
+code and shows a graphical representation of what takes how much time.
It uses the built-in Python
-https://docs.python.org/3.6/library/profile.html[cProfile] module and can show
-the output in four different ways:
+https://docs.python.org/3/library/profile.html[cProfile] module. It launches a
+qutebrowser instance, waits for it to exit and then shows the graph.
-* Raw profile file (`--profile-tool=none`)
+Available methods for visualization are:
+
+* https://jiffyclub.github.io/snakeviz/[SnakeViz] (`--profile-tool=snakeviz`, the default)
* https://pypi.python.org/pypi/pyprof2calltree/[pyprof2calltree] and http://kcachegrind.sourceforge.net/html/Home.html[KCacheGrind] (`--profile-tool=kcachegrind`)
-* https://jiffyclub.github.io/snakeviz/[SnakeViz] (`--profile-tool=snakeviz`)
-* https://github.com/jrfonseca/gprof2dot[gprof2dot] (needs `dot` from http://graphviz.org/[Graphviz] and http://feh.finalrewind.org/[feh])
+* https://github.com/jrfonseca/gprof2dot[gprof2dot] (`--profile-tool=gprof2dot`, needs `dot` from http://graphviz.org/[Graphviz] and http://feh.finalrewind.org/[feh])
+* https://github.com/nschloe/tuna[tuna] (`--profile-tool=tuna`)
+
+You can also save the binary profile data to a file (`--profile-tool=none`).
Debugging
~~~~~~~~~
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index fe86b577b..7d51c6b48 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -282,6 +282,7 @@
|<>|Alignment of the text inside of tabs.
|<>|Format to use for the tab title.
|<>|Format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined.
+|<>|Show tooltips on tabs.
|<>|Number of close tab actions to remember, per window (-1 for no maximum).
|<>|Width (in pixels or as percentage of the window) of the tab bar if it's vertical.
|<>|Wrap when changing tabs.
@@ -487,7 +488,7 @@ Default:
* +pass:[<Ctrl-E>]+: +pass:[open-editor]+
* +pass:[<Escape>]+: +pass:[leave-mode]+
-* +pass:[<Shift-Ins>]+: +pass:[insert-text {primary}]+
+* +pass:[<Shift-Ins>]+: +pass:[insert-text -- {primary}]+
- +pass:[normal]+:
* +pass:[']+: +pass:[enter-mode jump_mark]+
@@ -3538,6 +3539,15 @@ Type: <>
Default: +pass:[{index}]+
+[[tabs.tooltips]]
+=== tabs.tooltips
+Show tooltips on tabs.
+Note this setting only affects windows opened after it has been set.
+
+Type: <>
+
+Default: +pass:[true]+
+
[[tabs.undo_stack_size]]
=== tabs.undo_stack_size
Number of close tab actions to remember, per window (-1 for no maximum).
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 96c259059..4f431b72b 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-asn1crypto==0.24.0
+asn1crypto==1.1.0
bump2version==0.5.11
certifi==2019.9.11
cffi==1.12.3
@@ -9,7 +9,7 @@ colorama==0.4.1
cryptography==2.7
cssutils==1.0.2
github3.py==1.3.0
-hunter==3.0.1
+hunter==3.0.3
idna==2.8
jwcrypto==0.6.0
lxml==4.4.1
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index dd8de2282..9efe25ebf 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,13 +1,13 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==19.1.0
+attrs==19.2.0
entrypoints==0.3
flake8==3.7.8
flake8-bugbear==19.8.0
flake8-builtins==1.4.1
flake8-comprehensions==2.2.0
flake8-copyright==0.2.2
-flake8-debugger==3.1.0
+flake8-debugger==3.1.1
flake8-deprecated==1.3
flake8-docstrings==1.5.0
flake8-future-import==0.4.6
@@ -22,4 +22,4 @@ pycodestyle==2.5.0
pydocstyle==4.0.1
pyflakes==2.1.1
six==1.12.0
-snowballstemmer==1.9.1
+snowballstemmer==2.0.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index a3c7b9e8e..0b9beab8e 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
mypy==0.730
-mypy-extensions==0.4.1
+mypy-extensions==0.4.2
# PyQt5==5.11.3
# PyQt5-sip==4.19.19
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt
index 9be637f4f..4f75355ca 100644
--- a/misc/requirements/requirements-pip.txt
+++ b/misc/requirements/requirements-pip.txt
@@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==19.2
pyparsing==2.4.2
-setuptools==41.2.0
+setuptools==41.4.0
six==1.12.0
wheel==0.33.6
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 47c8fe74c..1f3c995a4 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-asn1crypto==0.24.0
+asn1crypto==1.1.0
astroid==2.3.1
certifi==2019.9.11
cffi==1.12.3
diff --git a/misc/requirements/requirements-pyqt-5.10.txt b/misc/requirements/requirements-pyqt-5.10.txt
index e098b3ac6..69c3ccbd0 100644
--- a/misc/requirements/requirements-pyqt-5.10.txt
+++ b/misc/requirements/requirements-pyqt-5.10.txt
@@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.10.1 # rq.filter: < 5.11
-sip==4.19.8
+sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.10.txt-raw b/misc/requirements/requirements-pyqt-5.10.txt-raw
index fe16524d8..4fbea8575 100644
--- a/misc/requirements/requirements-pyqt-5.10.txt-raw
+++ b/misc/requirements/requirements-pyqt-5.10.txt-raw
@@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.11
PyQt5 >= 5.10, < 5.11
+#@ filter: sip < 5
+sip < 5
diff --git a/misc/requirements/requirements-pyqt-5.11.txt b/misc/requirements/requirements-pyqt-5.11.txt
index f4be2fd88..0e3d2a07a 100644
--- a/misc/requirements/requirements-pyqt-5.11.txt
+++ b/misc/requirements/requirements-pyqt-5.11.txt
@@ -2,3 +2,4 @@
PyQt5==5.11.3 # rq.filter: < 5.12
PyQt5-sip==4.19.19
+sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.11.txt-raw b/misc/requirements/requirements-pyqt-5.11.txt-raw
index 40a81f952..347f1a472 100644
--- a/misc/requirements/requirements-pyqt-5.11.txt-raw
+++ b/misc/requirements/requirements-pyqt-5.11.txt-raw
@@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.12
PyQt5 >= 5.11, < 5.12
+#@ filter: sip < 5
+sip < 5
diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt
index dec26ca96..f09aa599f 100644
--- a/misc/requirements/requirements-pyqt-5.12.txt
+++ b/misc/requirements/requirements-pyqt-5.12.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
-PyQt5-sip==4.19.19
+PyQt5-sip==12.7.0
PyQtWebEngine==5.12.1 # rq.filter: < 5.13
diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt
index 3d2fe90a8..307e15030 100644
--- a/misc/requirements/requirements-pyqt-5.13.txt
+++ b/misc/requirements/requirements-pyqt-5.13.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.0 # rq.filter: < 5.14, != 5.13.1
-PyQt5-sip==4.19.19
+PyQt5-sip==12.7.0
PyQtWebEngine==5.13.1 # rq.filter: < 5.14
diff --git a/misc/requirements/requirements-pyqt-5.7.txt b/misc/requirements/requirements-pyqt-5.7.txt
index c3c24c208..703c95a92 100644
--- a/misc/requirements/requirements-pyqt-5.7.txt
+++ b/misc/requirements/requirements-pyqt-5.7.txt
@@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.7.1 # rq.filter: < 5.8
-sip==4.19.8
+sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.7.txt-raw b/misc/requirements/requirements-pyqt-5.7.txt-raw
index 5127ce517..745deb4b9 100644
--- a/misc/requirements/requirements-pyqt-5.7.txt-raw
+++ b/misc/requirements/requirements-pyqt-5.7.txt-raw
@@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.8
+#@ filter: sip < 5
PyQt5 >= 5.7, < 5.8
+sip < 5
diff --git a/misc/requirements/requirements-pyqt-5.9.txt b/misc/requirements/requirements-pyqt-5.9.txt
index 549df8f39..8f3258721 100644
--- a/misc/requirements/requirements-pyqt-5.9.txt
+++ b/misc/requirements/requirements-pyqt-5.9.txt
@@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.9.2 # rq.filter: < 5.10
-sip==4.19.8
+sip==4.19.8 # rq.filter: < 5
diff --git a/misc/requirements/requirements-pyqt-5.9.txt-raw b/misc/requirements/requirements-pyqt-5.9.txt-raw
index f30dcda63..45d4e0c10 100644
--- a/misc/requirements/requirements-pyqt-5.9.txt-raw
+++ b/misc/requirements/requirements-pyqt-5.9.txt-raw
@@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.10
PyQt5 >= 5.9, < 5.10
+#@ filter: sip < 5
+sip < 5
diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt
index fb5e06925..6cc0eb5b0 100644
--- a/misc/requirements/requirements-pyqt.txt
+++ b/misc/requirements/requirements-pyqt.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.0 # rq.filter: != 5.13.1
-PyQt5-sip==4.19.19
+PyQt5-sip==12.7.0
PyQtWebEngine==5.13.1
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index b8715149b..2cc0d5c39 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -1,22 +1,21 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.12
-attrs==19.1.0
Babel==2.7.0
certifi==2019.9.11
chardet==3.0.4
docutils==0.15.2
idna==2.8
imagesize==1.1.0
-Jinja2==2.10.1
+Jinja2==2.10.3
MarkupSafe==1.1.1
packaging==19.2
Pygments==2.4.2
pyparsing==2.4.2
-pytz==2019.2
+pytz==2019.3
requests==2.22.0
six==1.12.0
-snowballstemmer==1.9.1
+snowballstemmer==2.0.0
Sphinx==2.2.0
sphinxcontrib-applehelp==1.0.1
sphinxcontrib-devhelp==1.0.1
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index f42d56dfc..0e85429da 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,21 +1,21 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
atomicwrites==1.3.0
-attrs==19.1.0
-backports.functools-lru-cache==1.5
-beautifulsoup4==4.8.0
-cheroot==7.0.0
+attrs==19.2.0
+beautifulsoup4==4.8.1
+cheroot==8.1.0
Click==7.0
# colorama==0.4.1
coverage==4.5.4
EasyProcess==0.2.7
Flask==1.1.1
glob2==0.7
-hunter==3.0.1
-hypothesis==4.37.0
+hunter==3.0.3
+hypothesis==4.40.0
importlib-metadata==0.23
itsdangerous==1.1.0
-# Jinja2==2.10.1
+jaraco.functools==2.0
+# Jinja2==2.10.3
Mako==1.1.0
manhole==1.6.0
# MarkupSafe==1.1.1
@@ -27,12 +27,12 @@ pluggy==0.13.0
py==1.8.0
py-cpuinfo==5.0.0
pyparsing==2.4.2
-pytest==5.2.0
+pytest==5.2.1
pytest-bdd==3.2.1
pytest-benchmark==3.2.2
-pytest-cov==2.7.1
+pytest-cov==2.8.1
pytest-instafail==0.4.1
-pytest-mock==1.11.0
+pytest-mock==1.11.1
pytest-qt==3.2.2
pytest-repeat==0.8.0
pytest-rerunfailures==7.0
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 96cf1d2a3..885f58e5a 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -1,6 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==19.1.0
filelock==3.0.12
importlib-metadata==0.23
more-itertools==7.2.0
diff --git a/misc/userscripts/qute-keepass b/misc/userscripts/qute-keepass
index 03f9152cc..485d83e05 100755
--- a/misc/userscripts/qute-keepass
+++ b/misc/userscripts/qute-keepass
@@ -228,7 +228,7 @@ def run(args):
args.io_encoding)
if selection not in candidates_map:
- stderr("'{}' was not a valid entry!").format(selection)
+ stderr("'{}' was not a valid entry!".format(selection))
return ExitCodes.USER_QUIT
selection = candidates_map[selection]
diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass
index 4abad6de7..a22cd7187 100755
--- a/misc/userscripts/qute-pass
+++ b/misc/userscripts/qute-pass
@@ -72,6 +72,8 @@ argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
help='Encoding used to communicate with subprocesses')
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
help='Merge pass candidates for fully-qualified and registered domain name')
+argument_parser.add_argument('--extra-url-suffixes', '-s', default='',
+ help='Comma-separated string containing extra suffixes (e.g local)')
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
@@ -145,7 +147,8 @@ def main(arguments):
argument_parser.print_help()
return ExitCodes.FAILURE
- extract_result = tldextract.extract(arguments.url)
+ extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(','))
+ extract_result = extractor(arguments.url)
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us
password_store_path = os.path.expanduser(arguments.password_store)
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 0c33453ad..efd086ce0 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -49,12 +49,13 @@ const HEADER = `
`;
const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts');
const tmpFile = path.join(scriptsDir, '/readability.html');
+const domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"}
if (!fs.existsSync(scriptsDir)){
fs.mkdirSync(scriptsDir);
}
-JSDOM.fromFile(process.env.QUTE_HTML, { url: process.env.QUTE_URL }).then(dom => {
+JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => {
let reader = new Readability(dom.window.document);
let article = reader.parse();
let content = util.format(HEADER, article.title) + article.content;
diff --git a/mypy.ini b/mypy.ini
index 8f5f8e1c6..c47e2f7eb 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -58,10 +58,26 @@ disallow_subclassing_any = False
disallow_untyped_defs = True
disallow_incomplete_defs = True
+[mypy-qutebrowser.browser.hints]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
[mypy-qutebrowser.misc.objects]
disallow_untyped_defs = True
disallow_incomplete_defs = True
+[mypy-qutebrowser.misc.debugcachestats]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
+[mypy-qutebrowser.misc.utilcmds]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
+[mypy-qutebrowser.misc.throttle]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
[mypy-qutebrowser.commands.cmdutils]
disallow_untyped_defs = True
disallow_incomplete_defs = True
@@ -93,3 +109,19 @@ disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
+
+[mypy-qutebrowser.keyinput.*]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
+[mypy-qutebrowser.utils.*]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
+[mypy-qutebrowser.misc.backendproblem]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+
+[mypy-qutebrowser.mainwindow.statusbar.command]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index cf35f918f..831a5cca2 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -47,11 +47,13 @@ import tempfile
import atexit
import datetime
import tokenize
+import argparse
+import typing
from PyQt5.QtWidgets import QApplication, QWidget
-from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow
-from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
- QObject, QEvent, pyqtSignal, Qt)
+from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow, QKeyEvent
+from PyQt5.QtCore import (pyqtSlot, QTimer, QUrl, QObject, QEvent, pyqtSignal,
+ Qt)
try:
import hunter
except ImportError:
@@ -83,7 +85,7 @@ from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
-q_app = None
+q_app = typing.cast(QApplication, None)
def run(args):
@@ -91,8 +93,9 @@ def run(args):
if args.temp_basedir:
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
- quitter = Quitter(args)
- objreg.register('quitter', quitter)
+ quitter = Quitter(args=args)
+ objreg.register('quitter', quitter, command_only=True)
+ quitter.shutting_down.connect(log.shutdown_log)
log.init.debug("Initializing directories...")
standarddir.init(args)
@@ -115,13 +118,14 @@ def run(args):
crash_handler = crashsignal.CrashHandler(
app=q_app, quitter=quitter, args=args, parent=q_app)
+ objreg.register('crash-handler', crash_handler, command_only=True)
crash_handler.activate()
- objreg.register('crash-handler', crash_handler)
+ quitter.shutting_down.connect(crash_handler.shutdown)
signal_handler = crashsignal.SignalHandler(app=q_app, quitter=quitter,
parent=q_app)
signal_handler.activate()
- objreg.register('signal-handler', signal_handler)
+ quitter.shutting_down.connect(signal_handler.deactivate)
try:
server = ipc.send_or_listen(args)
@@ -136,11 +140,12 @@ def run(args):
"Backend from the running instance will be used")
sys.exit(usertypes.Exit.ok)
else:
+ quitter.shutting_down.connect(server.shutdown)
server.got_args.connect(lambda args, target_arg, cwd:
process_pos_args(args, cwd=cwd, via_ipc=True,
target_arg=target_arg))
- init(args, crash_handler)
+ init(args=args, crash_handler=crash_handler, quitter=quitter)
ret = qt_mainloop()
return ret
@@ -154,24 +159,23 @@ def qt_mainloop():
return q_app.exec_()
-def init(args, crash_handler):
- """Initialize everything.
-
- Args:
- args: The argparse namespace.
- crash_handler: The CrashHandler instance.
- """
+def init(*, args: argparse.Namespace,
+ crash_handler: crashsignal.CrashHandler,
+ quitter: 'Quitter'):
+ """Initialize everything."""
log.init.debug("Starting init...")
crash_handler.init_faulthandler()
q_app.setQuitOnLastWindowClosed(False)
+ quitter.shutting_down.connect(QApplication.closeAllWindows)
+
_init_icon()
loader.init()
loader.load_components()
try:
- _init_modules(args, crash_handler)
+ _init_modules(args=args, crash_handler=crash_handler, quitter=quitter)
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
error.handle_fatal_exc(e, args, "Error while initializing!",
pre_text="Error while initializing")
@@ -179,17 +183,17 @@ def init(args, crash_handler):
log.init.debug("Initializing eventfilter...")
event_filter = EventFilter(q_app)
- q_app.installEventFilter(event_filter)
- objreg.register('event-filter', event_filter)
+ event_filter.install()
+ quitter.shutting_down.connect(event_filter.shutdown)
log.init.debug("Connecting signals...")
- q_app.focusChanged.connect(on_focus_changed)
+ q_app.focusChanged.connect(on_focus_changed) # type: ignore
_process_args(args)
- QDesktopServices.setUrlHandler('http', open_desktopservices_url)
- QDesktopServices.setUrlHandler('https', open_desktopservices_url)
- QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
+ for scheme in ['http', 'https', 'qute']:
+ QDesktopServices.setUrlHandler(
+ scheme, open_desktopservices_url) # type: ignore
log.init.debug("Init done!")
crash_handler.raise_crashdlg()
@@ -217,8 +221,8 @@ def _process_args(args):
"""Open startpage etc. and process commandline args."""
if not args.override_restore:
_load_session(args.session)
- session_manager = objreg.get('session-manager')
- if not session_manager.did_load:
+
+ if not sessions.session_manager.did_load:
log.init.debug("Initializing main window...")
if config.val.content.private_browsing and qtutils.is_single_process():
err = Exception("Private windows are unavailable with "
@@ -244,8 +248,7 @@ def _load_session(name):
Args:
name: The name of the session to load, or None to read state file.
"""
- session_manager = objreg.get('session-manager')
- if name is None and session_manager.exists('_autosave'):
+ if name is None and sessions.session_manager.exists('_autosave'):
name = '_autosave'
elif name is None:
try:
@@ -256,7 +259,7 @@ def _load_session(name):
return
try:
- session_manager.load(name)
+ sessions.session_manager.load(name)
except sessions.SessionNotFoundError:
message.error("Session {} not found!".format(name))
except sessions.SessionError as e:
@@ -267,7 +270,7 @@ def _load_session(name):
pass
# If this was a _restart session, delete it.
if name == '_restart':
- session_manager.delete('_restart')
+ sessions.session_manager.delete('_restart')
def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
@@ -425,20 +428,22 @@ def open_desktopservices_url(url):
tabbed_browser.tabopen(url)
-def _init_modules(args, crash_handler):
+def _init_modules(*, args, crash_handler, quitter):
"""Initialize all 'modules' which need to be initialized.
Args:
args: The argparse namespace.
crash_handler: The CrashHandler instance.
+ quitter: The Quitter instance.
"""
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(q_app)
objreg.register('save-manager', save_manager)
+ quitter.shutting_down.connect(save_manager.shutdown)
configinit.late_init(save_manager)
log.init.debug("Checking backend requirements...")
- backendproblem.init()
+ backendproblem.init(quitter=quitter, args=args, save_manager=save_manager)
log.init.debug("Initializing prompts...")
prompt.init()
@@ -448,13 +453,15 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing proxy...")
proxy.init()
+ quitter.shutting_down.connect(proxy.shutdown)
log.init.debug("Initializing downloads...")
downloads.init()
+ quitter.shutting_down.connect(downloads.shutdown)
log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge()
- objreg.register('readline-bridge', readline_bridge)
+ objreg.register('readline-bridge', readline_bridge, command_only=True)
try:
log.init.debug("Initializing SQL...")
@@ -471,9 +478,11 @@ def _init_modules(args, crash_handler):
cmdhistory.init()
log.init.debug("Initializing sessions...")
sessions.init(q_app)
+ quitter.shutting_down.connect(sessions.shutdown)
log.init.debug("Initializing websettings...")
websettings.init(args)
+ quitter.shutting_down.connect(websettings.shutdown)
if not args.no_err_windows:
crash_handler.display_faulthandler()
@@ -487,14 +496,10 @@ def _init_modules(args, crash_handler):
objreg.register('bookmark-manager', bookmark_manager)
log.init.debug("Initializing cookies...")
- cookie_jar = cookies.CookieJar(q_app)
- ram_cookie_jar = cookies.RAMCookieJar(q_app)
- objreg.register('cookie-jar', cookie_jar)
- objreg.register('ram-cookie-jar', ram_cookie_jar)
+ cookies.init(q_app)
log.init.debug("Initializing cache...")
- diskcache = cache.DiskCache(standarddir.cache(), parent=q_app)
- objreg.register('cache', diskcache)
+ cache.init(q_app)
log.init.debug("Initializing downloads...")
download_manager = qtnetworkdownloads.DownloadManager(parent=q_app)
@@ -509,30 +514,35 @@ def _init_modules(args, crash_handler):
browsertab.init()
-class Quitter:
+class Quitter(QObject):
"""Utility class to quit/restart the QApplication.
Attributes:
quit_status: The current quitting status.
- _shutting_down: Whether we're currently shutting down.
+ _is_shutting_down: Whether we're currently shutting down.
_args: The argparse namespace.
"""
- def __init__(self, args):
+ shutting_down = pyqtSignal() # Emitted immediately before shut down
+
+ def __init__(self, *,
+ args: argparse.Namespace,
+ parent: QObject = None) -> None:
+ super().__init__(parent)
self.quit_status = {
'crash': True,
'tabs': False,
'main': False,
}
- self._shutting_down = False
+ self._is_shutting_down = False
self._args = args
- def on_last_window_closed(self):
+ def on_last_window_closed(self) -> None:
"""Slot which gets invoked when the last window was closed."""
self.shutdown(last_window=True)
- def _compile_modules(self):
+ def _compile_modules(self) -> None:
"""Compile all modules to catch SyntaxErrors."""
if os.path.basename(sys.argv[0]) == 'qutebrowser':
# Launched via launcher script
@@ -551,8 +561,12 @@ class Quitter:
with tokenize.open(os.path.join(dirpath, fn)) as f:
compile(f.read(), fn, 'exec')
- def _get_restart_args(self, pages=(), session=None, override_args=None):
- """Get the current working directory and args to relaunch qutebrowser.
+ def _get_restart_args(
+ self, pages: typing.Iterable[str] = (),
+ session: str = None,
+ override_args: typing.Mapping[str, str] = None
+ ) -> typing.Sequence[str]:
+ """Get args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
@@ -560,29 +574,18 @@ class Quitter:
override_args: Argument overrides as a dict.
Return:
- An (args, cwd) tuple.
- args: The commandline as a list of strings.
- cwd: The current working directory as a string.
+ The commandline as a list of strings.
"""
if os.path.basename(sys.argv[0]) == 'qutebrowser':
# Launched via launcher script
args = [sys.argv[0]]
- cwd = None
elif hasattr(sys, 'frozen'):
args = [sys.executable]
- cwd = os.path.abspath(os.path.dirname(sys.executable))
else:
args = [sys.executable, '-m', 'qutebrowser']
- cwd = os.path.join(
- os.path.abspath(os.path.dirname(qutebrowser.__file__)), '..')
- if not os.path.isdir(cwd):
- # Probably running from a python egg. Let's fallback to
- # cwd=None and see if that works out.
- # See https://github.com/qutebrowser/qutebrowser/issues/323
- cwd = None
# Add all open pages so they get reopened.
- page_args = []
+ page_args = [] # type: typing.MutableSequence[str]
for win in pages:
page_args.extend(win)
page_args.append('')
@@ -616,12 +619,11 @@ class Quitter:
args += ['--json-args', data]
log.destroy.debug("args: {}".format(args))
- log.destroy.debug("cwd: {}".format(cwd))
- return args, cwd
+ return args
@cmdutils.register(instance='quitter', name='restart')
- def restart_cmd(self):
+ def restart_cmd(self) -> None:
"""Restart qutebrowser while keeping existing tabs open."""
try:
ok = self.restart(session='_restart')
@@ -636,7 +638,9 @@ class Quitter:
if ok:
self.shutdown(restart=True)
- def restart(self, pages=(), session=None, override_args=None):
+ def restart(self, pages: typing.Sequence[str] = (),
+ session: str = None,
+ override_args: typing.Mapping[str, str] = None) -> bool:
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
@@ -662,20 +666,17 @@ class Quitter:
# Save the session if one is given.
if session is not None:
- session_manager = objreg.get('session-manager')
- session_manager.save(session, with_private=True)
+ sessions.session_manager.save(session, with_private=True)
# Make sure we're not accepting a connection from the new process
# before we fully exited.
+ assert ipc.server is not None
ipc.server.shutdown()
# Open a new process and immediately shutdown the existing one
try:
- args, cwd = self._get_restart_args(pages, session, override_args)
- if cwd is None:
- subprocess.Popen(args)
- else:
- subprocess.Popen(args, cwd=cwd)
+ args = self._get_restart_args(pages, session, override_args)
+ subprocess.Popen(args)
except OSError:
log.destroy.exception("Failed to restart")
return False
@@ -684,7 +685,9 @@ class Quitter:
@cmdutils.register(instance='quitter', name='quit')
@cmdutils.argument('session', completion=miscmodels.session)
- def quit(self, save=False, session=None):
+ def quit(self,
+ save: bool = False,
+ session: sessions.ArgType = None) -> None:
"""Quit qutebrowser.
Args:
@@ -701,8 +704,10 @@ class Quitter:
else:
self.shutdown()
- def shutdown(self, status=0, session=None, last_window=False,
- restart=False):
+ def shutdown(self, status: int = 0,
+ session: sessions.ArgType = None,
+ last_window: bool = False,
+ restart: bool = False) -> None:
"""Quit qutebrowser.
Args:
@@ -712,19 +717,20 @@ class Quitter:
closing.
restart: If we're planning to restart.
"""
- if self._shutting_down:
+ if self._is_shutting_down:
return
- self._shutting_down = True
+ self._is_shutting_down = True
log.destroy.debug("Shutting down with status {}, session {}...".format(
status, session))
- session_manager = objreg.get('session-manager', None)
- if session_manager is not None:
+ if sessions.session_manager is not None:
if session is not None:
- session_manager.save(session, last_window=last_window,
- load_next_time=True)
+ sessions.session_manager.save(session,
+ last_window=last_window,
+ load_next_time=True)
elif config.val.auto_save.session:
- session_manager.save(sessions.default, last_window=last_window,
- load_next_time=True)
+ sessions.session_manager.save(sessions.default,
+ last_window=last_window,
+ load_next_time=True)
if prompt.prompt_queue.shutdown():
# If shutdown was called while we were asking a question, we're in
@@ -741,64 +747,24 @@ class Quitter:
# event loop, so we can shut down immediately.
self._shutdown(status, restart=restart)
- def _shutdown(self, status, restart): # noqa
+ def _shutdown(self, status: int, restart: bool) -> None: # noqa
"""Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...")
if q_app is None:
# No QApplication exists yet, so quit hard.
sys.exit(status)
- # Remove eventfilter
- try:
- log.destroy.debug("Removing eventfilter...")
- event_filter = objreg.get('event-filter', None)
- if event_filter is not None:
- q_app.removeEventFilter(event_filter)
- except AttributeError:
- pass
- # Close all windows
- QApplication.closeAllWindows()
- # Shut down IPC
- try:
- ipc.server.shutdown()
- except KeyError:
- pass
- # Save everything
- try:
- save_manager = objreg.get('save-manager')
- except KeyError:
- log.destroy.debug("Save manager not initialized yet, so not "
- "saving anything.")
- else:
- for key in save_manager.saveables:
- try:
- save_manager.save(key, is_exit=True)
- except OSError as e:
- error.handle_fatal_exc(
- e, self._args, "Error while saving!",
- pre_text="Error while saving {}".format(key))
- # Disable storage so removing tempdir will work
- websettings.shutdown()
- # Disable application proxy factory to fix segfaults with Qt 5.10.1
- proxy.shutdown()
- # Re-enable faulthandler to stdout, then remove crash log
- log.destroy.debug("Deactivating crash log...")
- objreg.get('crash-handler').destroy_crashlogfile()
+
+ # Tell everything to shut itself down
+ self.shutting_down.emit()
+
# Delete temp basedir
if ((self._args.temp_basedir or self._args.temp_basedir_restarted) and
not restart):
atexit.register(shutil.rmtree, self._args.basedir,
ignore_errors=True)
- # Delete temp download dir
- downloads.temp_download_manager.cleanup()
- # If we don't kill our custom handler here we might get segfaults
- log.destroy.debug("Deactivating message handler...")
- qInstallMessageHandler(None)
+
# Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...")
- objreg.get('signal-handler').deactivate()
- session_manager = objreg.get('session-manager', None)
- if session_manager is not None:
- session_manager.delete_autosave()
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
QTimer.singleShot(0, functools.partial(q_app.exit, status))
@@ -827,10 +793,9 @@ class Application(QApplication):
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args)
- log.init.debug("Initializing application...")
+ objects.args = args
- objreg.register('args', args)
- objreg.register('app', self)
+ log.init.debug("Initializing application...")
self.launch_time = datetime.datetime.now()
self.focusObjectChanged.connect(self.on_focus_object_changed)
@@ -883,7 +848,7 @@ class EventFilter(QObject):
event.
"""
- def __init__(self, parent=None):
+ def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._activated = True
self._handlers = {
@@ -892,7 +857,14 @@ class EventFilter(QObject):
QEvent.ShortcutOverride: self._handle_key_event,
}
- def _handle_key_event(self, event):
+ def install(self):
+ q_app.installEventFilter(self)
+
+ @pyqtSlot()
+ def shutdown(self):
+ q_app.removeEventFilter(self)
+
+ def _handle_key_event(self, event: QKeyEvent) -> bool:
"""Handle a key press/release event.
Args:
@@ -912,7 +884,7 @@ class EventFilter(QObject):
# No window available yet, or not a MainWindow
return False
- def eventFilter(self, obj, event):
+ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Handle an event.
Args:
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 4f8d31c81..b04729a59 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -40,8 +40,8 @@ from qutebrowser.keyinput import modeman
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 eventfilter, hints
+from qutebrowser.misc import miscwidgets, objects, sessions
+from qutebrowser.browser import eventfilter
from qutebrowser.qt import sip
if typing.TYPE_CHECKING:
@@ -374,7 +374,7 @@ class AbstractZoom(QObject):
levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.val.zoom.default
- def apply_offset(self, offset: int) -> None:
+ def apply_offset(self, offset: int) -> int:
"""Increase/Decrease the zoom level by the given offset.
Args:
@@ -383,7 +383,7 @@ class AbstractZoom(QObject):
Return:
The new zoom percentage.
"""
- level = self._neighborlist.getitem(offset)
+ level = self._neighborlist.getitem(offset) # type: int
self.set_factor(float(level) / 100, fuzzyval=False)
return level
@@ -888,17 +888,12 @@ class AbstractTab(QWidget):
self._tab_event_filter = eventfilter.TabEventFilter(
self, parent=self)
self.backend = None
+
# If true, this tab has been requested to be removed (or is removed).
self.pending_removal = False
self.shutting_down.connect(functools.partial(
setattr, self, 'pending_removal', True))
- # FIXME:qtwebengine Should this be public api via self.hints?
- # Also, should we get it out of objreg?
- hintmanager = hints.HintManager(win_id, self.tab_id, parent=self)
- objreg.register('hintmanager', hintmanager, scope='tab',
- window=self.win_id, tab=self.tab_id)
-
self.before_load_started.connect(self._on_before_load_started)
def _set_widget(self, widget: QWidget) -> None:
@@ -1015,13 +1010,9 @@ class AbstractTab(QWidget):
# https://github.com/qutebrowser/qutebrowser/issues/3498
return
- try:
- sess_manager = objreg.get('session-manager')
- except KeyError:
- # https://github.com/qutebrowser/qutebrowser/issues/4311
- return
+ if sessions.session_manager is not None:
+ sessions.session_manager.save_autosave()
- sess_manager.save_autosave()
self.load_finished.emit(ok)
if not self.title():
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 636d5e918..856f919c6 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -849,7 +849,7 @@ class CommandDispatcher:
idx = int(index_parts[1])
elif len(index_parts) == 1:
idx = int(index_parts[0])
- active_win = objreg.get('app').activeWindow()
+ active_win = QApplication.activeWindow()
if active_win is None:
# Not sure how you enter a command without an active window...
raise cmdutils.CommandError(
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 9dc143140..38fa806a9 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -77,6 +77,11 @@ def init():
config.instance.changed.connect(_clear_last_used)
+@pyqtSlot()
+def shutdown():
+ temp_download_manager.cleanup()
+
+
@config.change_filter('downloads.location.directory', function=True)
def _clear_last_used():
global last_used_directory
diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py
index fc45fe26c..f65733067 100644
--- a/qutebrowser/browser/downloadview.py
+++ b/qutebrowser/browser/downloadview.py
@@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads
from qutebrowser.config import config
-from qutebrowser.utils import qtutils, utils, objreg
+from qutebrowser.utils import qtutils, utils
from qutebrowser.qt import sip
@@ -59,7 +59,6 @@ class DownloadView(QListView):
Attributes:
_menu: The QMenu which is currently displayed.
- _model: The currently set model.
"""
STYLESHEET = """
@@ -73,7 +72,7 @@ class DownloadView(QListView):
}
"""
- def __init__(self, win_id, parent=None):
+ def __init__(self, model, parent=None):
super().__init__(parent)
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion'))
@@ -85,7 +84,6 @@ class DownloadView(QListView):
self.setFlow(QListView.LeftToRight)
self.setSpacing(1)
self._menu = None
- model = objreg.get('download-model', scope='window', window=win_id)
model.rowsInserted.connect(functools.partial(update_geometry, self))
model.rowsRemoved.connect(functools.partial(update_geometry, self))
model.dataChanged.connect(functools.partial(update_geometry, self))
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 7ccfe6b0a..f7d75c09f 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -20,6 +20,7 @@
"""A HintManager to draw hints over links."""
import collections
+import typing
import functools
import os
import re
@@ -37,6 +38,8 @@ from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
+if typing.TYPE_CHECKING:
+ from qutebrowser.browser import browsertab
Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
@@ -50,7 +53,7 @@ class HintingError(Exception):
"""Exception raised on errors during hinting."""
-def on_mode_entered(mode, win_id):
+def on_mode_entered(mode: usertypes.KeyMode, win_id: int) -> None:
"""Stop hinting when insert mode was entered."""
if mode == usertypes.KeyMode.insert:
modeman.leave(win_id, usertypes.KeyMode.hint, 'insert mode',
@@ -66,7 +69,8 @@ class HintLabel(QLabel):
_context: The current hinting context.
"""
- def __init__(self, elem, context):
+ def __init__(self, elem: webelem.AbstractWebElement,
+ context: 'HintContext') -> None:
super().__init__(parent=context.tab)
self._context = context
self.elem = elem
@@ -81,14 +85,14 @@ class HintLabel(QLabel):
self._move_to_elem()
self.show()
- def __repr__(self):
+ def __repr__(self) -> str:
try:
text = self.text()
except RuntimeError:
text = ''
return utils.get_repr(self, elem=self.elem, text=text)
- def update_text(self, matched, unmatched):
+ def update_text(self, matched: str, unmatched: str) -> None:
"""Set the text for the hint.
Args:
@@ -112,7 +116,7 @@ class HintLabel(QLabel):
self.adjustSize()
@pyqtSlot()
- def _move_to_elem(self):
+ def _move_to_elem(self) -> None:
"""Reposition the label to its element."""
if not self.elem.has_frame():
# This sometimes happens for some reason...
@@ -123,7 +127,7 @@ class HintLabel(QLabel):
rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y())
- def cleanup(self):
+ def cleanup(self) -> None:
"""Clean up this element and hide it."""
self.hide()
self.deleteLater()
@@ -159,22 +163,22 @@ class HintContext:
group: The group of web elements to hint.
"""
- all_labels = attr.ib(attr.Factory(list))
- labels = attr.ib(attr.Factory(dict))
- target = attr.ib(None)
- baseurl = attr.ib(None)
- to_follow = attr.ib(None)
- rapid = attr.ib(False)
- first_run = attr.ib(True)
- add_history = attr.ib(False)
- filterstr = attr.ib(None)
- args = attr.ib(attr.Factory(list))
- tab = attr.ib(None)
- group = attr.ib(None)
- hint_mode = attr.ib(None)
- first = attr.ib(False)
+ all_labels = attr.ib(attr.Factory(list)) # type: typing.List[HintLabel]
+ labels = attr.ib(attr.Factory(dict)) # type: typing.Dict[str, HintLabel]
+ target = attr.ib(None) # type: Target
+ baseurl = attr.ib(None) # type: QUrl
+ to_follow = attr.ib(None) # type: str
+ rapid = attr.ib(False) # type: bool
+ first_run = attr.ib(True) # type: bool
+ add_history = attr.ib(False) # type: bool
+ filterstr = attr.ib(None) # type: str
+ args = attr.ib(attr.Factory(list)) # type: typing.List[str]
+ tab = attr.ib(None) # type: browsertab.AbstractTab
+ group = attr.ib(None) # type: str
+ hint_mode = attr.ib(None) # type: str
+ first = attr.ib(False) # type: bool
- def get_args(self, urlstr):
+ def get_args(self, urlstr: str) -> typing.Sequence[str]:
"""Get the arguments, with {hint-url} replaced by the given URL."""
args = []
for arg in self.args:
@@ -187,16 +191,12 @@ class HintActions:
"""Actions which can be done after selecting a hint."""
- def __init__(self, win_id):
+ def __init__(self, win_id: int) -> None:
self._win_id = win_id
- def click(self, elem, context):
- """Click an element.
-
- Args:
- elem: The QWebElement to click.
- context: The HintContext to use.
- """
+ def click(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
+ """Click an element."""
target_mapping = {
Target.normal: usertypes.ClickTarget.normal,
Target.current: usertypes.ClickTarget.normal,
@@ -225,20 +225,15 @@ class HintActions:
except webelem.Error as e:
raise HintingError(str(e))
- def yank(self, url, context):
- """Yank an element to the clipboard or primary selection.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
+ def yank(self, url: QUrl, context: HintContext) -> None:
+ """Yank an element to the clipboard or primary selection."""
sel = (context.target == Target.yank_primary and
utils.supports_selection())
flags = QUrl.FullyEncoded | QUrl.RemovePassword
if url.scheme() == 'mailto':
flags |= QUrl.RemoveScheme
- urlstr = url.toString(flags)
+ urlstr = url.toString(flags) # type: ignore
new_content = urlstr
@@ -257,26 +252,16 @@ class HintActions:
urlstr)
message.info(msg)
- def run_cmd(self, url, context):
- """Run the command based on a hint URL.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
- urlstr = url.toString(QUrl.FullyEncoded)
+ def run_cmd(self, url: QUrl, context: HintContext) -> None:
+ """Run the command based on a hint URL."""
+ urlstr = url.toString(QUrl.FullyEncoded) # type: ignore
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely(' '.join(args))
- def preset_cmd_text(self, url, context):
- """Preset a commandline text based on a hint URL.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
- urlstr = url.toDisplayString(QUrl.FullyEncoded)
+ def preset_cmd_text(self, url: QUrl, context: HintContext) -> None:
+ """Preset a commandline text based on a hint URL."""
+ urlstr = url.toDisplayString(QUrl.FullyEncoded) # type: ignore
args = context.get_args(urlstr)
text = ' '.join(args)
if text[0] not in modeparsers.STARTCHARS:
@@ -285,7 +270,8 @@ class HintActions:
cmd = objreg.get('status-command', scope='window', window=self._win_id)
cmd.set_cmd_text(text)
- def download(self, elem, context):
+ def download(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
"""Download a hint URL.
Args:
@@ -305,7 +291,8 @@ class HintActions:
download_manager.get(url, qnam=qnam, user_agent=user_agent,
prompt_download_directory=prompt)
- def call_userscript(self, elem, context):
+ def call_userscript(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
"""Call a userscript from a hint.
Args:
@@ -321,7 +308,7 @@ class HintActions:
}
url = elem.resolve_url(context.baseurl)
if url is not None:
- env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
+ env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) # type: ignore
try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
@@ -329,22 +316,28 @@ class HintActions:
except userscripts.Error as e:
raise HintingError(str(e))
- def delete(self, elem, _context):
+ def delete(self, elem: webelem.AbstractWebElement,
+ _context: HintContext) -> None:
elem.delete()
- def spawn(self, url, context):
+ def spawn(self, url: QUrl, context: HintContext) -> None:
"""Spawn a simple command from a hint.
Args:
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
- urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
+ urlstr = url.toString(
+ QUrl.FullyEncoded | QUrl.RemovePassword) # type: ignore
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely('spawn ' + ' '.join(args))
+_ElemsType = typing.Sequence[webelem.AbstractWebElement]
+_HintStringsType = typing.MutableSequence[str]
+
+
class HintManager(QObject):
"""Manage drawing hints over links or other elements.
@@ -379,12 +372,11 @@ class HintManager(QObject):
Target.delete: "Delete an element",
}
- def __init__(self, win_id, tab_id, parent=None):
+ def __init__(self, win_id: int, parent: QObject = None) -> None:
"""Constructor."""
super().__init__(parent)
self._win_id = win_id
- self._tab_id = tab_id
- self._context = None
+ self._context = None # type: typing.Optional[HintContext]
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
@@ -393,16 +385,18 @@ class HintManager(QObject):
window=win_id)
mode_manager.left.connect(self.on_mode_left)
- def _get_text(self):
+ def _get_text(self) -> str:
"""Get a hint text based on the current context."""
+ assert self._context is not None
text = self.HINT_TEXTS[self._context.target]
if self._context.rapid:
text += ' (rapid mode)'
text += '...'
return text
- def _cleanup(self):
+ def _cleanup(self) -> None:
"""Clean up after hinting."""
+ assert self._context is not None
for label in self._context.all_labels:
label.cleanup()
@@ -412,7 +406,7 @@ class HintManager(QObject):
message_bridge.maybe_reset_text(text)
self._context = None
- def _hint_strings(self, elems):
+ def _hint_strings(self, elems: _ElemsType) -> _HintStringsType:
"""Calculate the hint strings for elems.
Inspired by Vimium.
@@ -425,6 +419,8 @@ class HintManager(QObject):
"""
if not elems:
return []
+
+ assert self._context is not None
hint_mode = self._context.hint_mode
if hint_mode == 'word':
try:
@@ -442,7 +438,9 @@ class HintManager(QObject):
else:
return self._hint_linear(min_chars, chars, elems)
- def _hint_scattered(self, min_chars, chars, elems):
+ def _hint_scattered(self, min_chars: int,
+ chars: str,
+ elems: _ElemsType) -> _HintStringsType:
"""Produce scattered hint labels with variable length (like Vimium).
Args:
@@ -479,7 +477,9 @@ class HintManager(QObject):
return self._shuffle_hints(strings, len(chars))
- def _hint_linear(self, min_chars, chars, elems):
+ def _hint_linear(self, min_chars: int,
+ chars: str,
+ elems: _ElemsType) -> _HintStringsType:
"""Produce linear hint labels with constant length (like dwb).
Args:
@@ -493,7 +493,8 @@ class HintManager(QObject):
strings.append(self._number_to_hint_str(i, chars, needed))
return strings
- def _shuffle_hints(self, hints, length):
+ def _shuffle_hints(self, hints: _HintStringsType,
+ length: int) -> _HintStringsType:
"""Shuffle the given set of hints so that they're scattered.
Hints starting with the same character will be spread evenly throughout
@@ -508,15 +509,19 @@ class HintManager(QObject):
Return:
A list of shuffled hint strings.
"""
- buckets = [[] for i in range(length)]
+ buckets = [
+ [] for i in range(length)
+ ] # type: typing.Sequence[_HintStringsType]
for i, hint in enumerate(hints):
buckets[i % len(buckets)].append(hint)
- result = []
+ result = [] # type: _HintStringsType
for bucket in buckets:
result += bucket
return result
- def _number_to_hint_str(self, number, chars, digits=0):
+ def _number_to_hint_str(self, number: int,
+ chars: str,
+ digits: int = 0) -> str:
"""Convert a number like "8" into a hint string like "JK".
This is used to sequentially generate all of the hint text.
@@ -534,7 +539,7 @@ class HintManager(QObject):
A hint string.
"""
base = len(chars)
- hintstr = []
+ hintstr = [] # type: typing.MutableSequence[str]
remainder = 0
while True:
remainder = number % base
@@ -548,7 +553,7 @@ class HintManager(QObject):
hintstr.insert(0, chars[0])
return ''.join(hintstr)
- def _check_args(self, target, *args):
+ def _check_args(self, target: Target, *args: str) -> None:
"""Check the arguments passed to start() and raise if they're wrong.
Args:
@@ -568,7 +573,7 @@ class HintManager(QObject):
raise cmdutils.CommandError(
"'args' is only allowed with target userscript/spawn.")
- def _filter_matches(self, filterstr, elemstr):
+ def _filter_matches(self, filterstr: str, elemstr: str) -> bool:
"""Return True if `filterstr` matches `elemstr`."""
# Empty string and None always match
if not filterstr:
@@ -578,7 +583,7 @@ class HintManager(QObject):
# Do multi-word matching
return all(word in elemstr for word in filterstr.split())
- def _filter_matches_exactly(self, filterstr, elemstr):
+ def _filter_matches_exactly(self, filterstr: str, elemstr: str) -> bool:
"""Return True if `filterstr` exactly matches `elemstr`."""
# Empty string and None never match
if not filterstr:
@@ -587,7 +592,7 @@ class HintManager(QObject):
elemstr = elemstr.casefold()
return filterstr == elemstr
- def _start_cb(self, elems):
+ def _start_cb(self, elems: _ElemsType) -> None:
"""Initialize the elements and labels based on the context set."""
if self._context is None:
log.hints.debug("In _start_cb without context!")
@@ -603,10 +608,10 @@ class HintManager(QObject):
tabbed_browser = objreg.get('tabbed-browser', default=None,
scope='window', window=self._win_id)
tab = tabbed_browser.widget.currentWidget()
- if tab.tab_id != self._tab_id:
+ if tab.tab_id != self._context.tab.tab_id:
log.hints.debug(
"Current tab changed ({} -> {}) before _start_cb is run."
- .format(self._tab_id, tab.tab_id))
+ .format(self._context.tab.tab_id, tab.tab_id))
return
strings = self._hint_strings(elems)
@@ -635,11 +640,16 @@ class HintManager(QObject):
# to make auto_follow == 'always' work
self._handle_auto_follow()
- @cmdutils.register(instance='hintmanager', scope='tab', name='hint',
+ @cmdutils.register(instance='hintmanager', scope='window', name='hint',
star_args_optional=True, maxsplit=2)
def start(self, # pylint: disable=keyword-arg-before-vararg
- group='all', target=Target.normal, *args, mode=None,
- add_history=False, rapid=False, first=False):
+ group: str = 'all',
+ target: Target = Target.normal,
+ *args: str,
+ mode: str = None,
+ add_history: bool = False,
+ rapid: bool = False,
+ first: bool = False) -> None:
"""Start hinting.
Args:
@@ -755,7 +765,7 @@ class HintManager(QObject):
error_cb=lambda err: message.error(str(err)),
only_visible=True)
- def _get_hint_mode(self, mode):
+ def _get_hint_mode(self, mode: typing.Optional[str]) -> str:
"""Get the hinting mode to use based on a mode argument."""
if mode is None:
return config.val.hints.mode
@@ -767,15 +777,22 @@ class HintManager(QObject):
raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode
- def current_mode(self):
+ def current_mode(self) -> typing.Optional[str]:
"""Return the currently active hinting mode (or None otherwise)."""
if self._context is None:
return None
return self._context.hint_mode
- def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
+ def _handle_auto_follow(
+ self,
+ keystr: str = "",
+ filterstr: str = "",
+ visible: typing.Mapping[str, HintLabel] = None
+ ) -> None:
"""Handle the auto_follow option."""
+ assert self._context is not None
+
if visible is None:
visible = {string: label
for string, label in self._context.labels.items()
@@ -789,7 +806,7 @@ class HintManager(QObject):
if auto_follow == "always":
follow = True
elif auto_follow == "unique-match":
- follow = keystr or filterstr
+ follow = bool(keystr or filterstr)
elif auto_follow == "full-match":
elemstr = str(list(visible.values())[0].elem)
filter_match = self._filter_matches_exactly(filterstr, elemstr)
@@ -810,7 +827,8 @@ class HintManager(QObject):
# unpacking gets us the first (and only) key in the dict.
self._fire(*visible)
- def handle_partial_key(self, keystr):
+ @pyqtSlot(str)
+ def handle_partial_key(self, keystr: str) -> None:
"""Handle a new partial keypress."""
if self._context is None:
log.hints.debug("Got key without context!")
@@ -834,7 +852,7 @@ class HintManager(QObject):
pass
self._handle_auto_follow(keystr=keystr)
- def filter_hints(self, filterstr):
+ def filter_hints(self, filterstr: typing.Optional[str]) -> None:
"""Filter displayed hints according to a text.
Args:
@@ -844,6 +862,8 @@ class HintManager(QObject):
and `self._context.filterstr` are None, all hints are
shown.
"""
+ assert self._context is not None
+
if filterstr is None:
filterstr = self._context.filterstr
else:
@@ -889,12 +909,13 @@ class HintManager(QObject):
self._handle_auto_follow(filterstr=filterstr,
visible=self._context.labels)
- def _fire(self, keystr):
+ def _fire(self, keystr: str) -> None:
"""Fire a completed hint.
Args:
keystr: The keychain string to follow.
"""
+ assert self._context is not None
# Handlers which take a QWebElement
elem_handlers = {
Target.normal: self._actions.click,
@@ -956,15 +977,16 @@ class HintManager(QObject):
if self._context is not None:
self._context.first_run = False
- @cmdutils.register(instance='hintmanager', scope='tab',
+ @cmdutils.register(instance='hintmanager', scope='window',
modes=[usertypes.KeyMode.hint])
- def follow_hint(self, select=False, keystring=None):
+ def follow_hint(self, select: bool = False, keystring: str = None) -> None:
"""Follow a hint.
Args:
select: Only select the given hint, don't necessarily follow it.
keystring: The hint to follow, or None.
"""
+ assert self._context is not None
if keystring is None:
if self._context.to_follow is None:
raise cmdutils.CommandError("No hint to follow")
@@ -980,7 +1002,7 @@ class HintManager(QObject):
self._fire(keystring)
@pyqtSlot(usertypes.KeyMode)
- def on_mode_left(self, mode):
+ def on_mode_left(self, mode: usertypes.KeyMode) -> None:
"""Stop hinting when hinting mode was left."""
if mode != usertypes.KeyMode.hint or self._context is None:
# We have one HintManager per tab, so when this gets called,
@@ -999,12 +1021,12 @@ class WordHinter:
derived from the hinted element.
"""
- def __init__(self):
+ def __init__(self) -> None:
# will be initialized on first use.
- self.words = set()
+ self.words = set() # type: typing.Set[str]
self.dictionary = None
- def ensure_initialized(self):
+ def ensure_initialized(self) -> None:
"""Generate the used words if yet uninitialized."""
dictionary = config.val.hints.dictionary
if not self.words or self.dictionary != dictionary:
@@ -1031,8 +1053,11 @@ class WordHinter:
error = "Word hints requires reading the file at {}: {}"
raise HintingError(error.format(dictionary, str(e)))
- def extract_tag_words(self, elem):
+ def extract_tag_words(
+ self, elem: webelem.AbstractWebElement
+ ) -> typing.Iterator[str]:
"""Extract tag words form the given element."""
+ _extractor_type = typing.Callable[[webelem.AbstractWebElement], str]
attr_extractors = {
"alt": lambda elem: elem["alt"],
"name": lambda elem: elem["name"],
@@ -1041,7 +1066,7 @@ class WordHinter:
"src": lambda elem: elem["src"].split('/')[-1],
"href": lambda elem: elem["href"].split('/')[-1],
"text": str,
- }
+ } # type: typing.Mapping[str, _extractor_type]
extractable_attrs = collections.defaultdict(list, {
"img": ["alt", "title", "src"],
@@ -1055,7 +1080,10 @@ class WordHinter:
for attr in extractable_attrs[elem.tag_name()]
if attr in elem or attr == "text")
- def tag_words_to_hints(self, words):
+ def tag_words_to_hints(
+ self,
+ words: typing.Iterable[str]
+ ) -> typing.Iterator[str]:
"""Take words and transform them to proper hints if possible."""
for candidate in words:
if not candidate:
@@ -1066,13 +1094,20 @@ class WordHinter:
if 4 < match.end() - match.start() < 8:
yield candidate[match.start():match.end()].lower()
- def any_prefix(self, hint, existing):
+ def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool:
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
- def filter_prefixes(self, hints, existing):
+ def filter_prefixes(
+ self,
+ hints: typing.Iterable[str],
+ existing: typing.Iterable[str]
+ ) -> typing.Iterator[str]:
+ """Filter hints which don't start with the given prefix."""
return (h for h in hints if not self.any_prefix(h, existing))
- def new_hint_for(self, elem, existing, fallback):
+ def new_hint_for(self, elem: webelem.AbstractWebElement,
+ existing: typing.Iterable[str],
+ fallback: typing.Iterable[str]) -> typing.Optional[str]:
"""Return a hint for elem, not conflicting with the existing."""
new = self.tag_words_to_hints(self.extract_tag_words(elem))
new_no_prefixes = self.filter_prefixes(new, existing)
@@ -1081,7 +1116,7 @@ class WordHinter:
return (next(new_no_prefixes, None) or
next(fallback_no_prefixes, None))
- def hint(self, elems):
+ def hint(self, elems: _ElemsType) -> _HintStringsType:
"""Produce hint labels based on the html tags.
Produce hint words based on the link text and random words
@@ -1096,7 +1131,7 @@ class WordHinter:
"""
self.ensure_initialized()
hints = []
- used_hints = set()
+ used_hints = set() # type: typing.Set[str]
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints, words)
diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py
index a14448512..2bd1a6325 100644
--- a/qutebrowser/browser/network/proxy.py
+++ b/qutebrowser/browser/network/proxy.py
@@ -19,21 +19,25 @@
"""Handling of proxies."""
+import typing
-from PyQt5.QtCore import QUrl
+from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
-from qutebrowser.utils import objreg, message, usertypes, urlutils
+from qutebrowser.utils import message, usertypes, urlutils
from qutebrowser.misc import objects
from qutebrowser.browser.network import pac
+application_factory = None
+
+
def init():
"""Set the application wide proxy factory."""
- proxy_factory = ProxyFactory()
- objreg.register('proxy-factory', proxy_factory)
- QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
+ global application_factory
+ application_factory = ProxyFactory()
+ QNetworkProxyFactory.setApplicationProxyFactory(application_factory)
config.instance.changed.connect(_warn_for_pac)
_warn_for_pac()
@@ -48,6 +52,7 @@ def _warn_for_pac():
message.error("PAC support isn't implemented for QtWebEngine yet!")
+@pyqtSlot()
def shutdown():
QNetworkProxyFactory.setApplicationProxyFactory(None)
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
index 747f3162e..b36fb7389 100644
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -27,7 +27,6 @@ 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
if typing.TYPE_CHECKING:
@@ -89,7 +88,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
def __repr__(self) -> str:
try:
- html = utils.compact_text(self.outer_xml(), 500)
+ html = utils.compact_text(
+ self.outer_xml(), 500) # type: typing.Optional[str]
except Error:
html = None
return utils.get_repr(self, html=html)
@@ -385,6 +385,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:
+ from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
window.show()
window.tabbed_browser.tabopen(url)
diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py
index 4844ea0d3..c77ae2616 100644
--- a/qutebrowser/browser/webkit/cache.py
+++ b/qutebrowser/browser/webkit/cache.py
@@ -24,7 +24,10 @@ import os.path
from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.config import config
-from qutebrowser.utils import utils, qtutils
+from qutebrowser.utils import utils, qtutils, standarddir
+
+
+diskcache = None
class DiskCache(QNetworkDiskCache):
@@ -52,3 +55,9 @@ class DiskCache(QNetworkDiskCache):
if not qtutils.version_check('5.9', compiled=False):
size = 0 # pragma: no cover
self.setMaximumCacheSize(size)
+
+
+def init(parent):
+ """Initialize the global cache."""
+ global diskcache
+ diskcache = DiskCache(standarddir.cache(), parent=parent)
diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py
index 9dce9bc11..bd9434018 100644
--- a/qutebrowser/browser/webkit/cookies.py
+++ b/qutebrowser/browser/webkit/cookies.py
@@ -27,6 +27,10 @@ from qutebrowser.utils import utils, standarddir, objreg
from qutebrowser.misc import lineparser
+cookie_jar = None
+ram_cookie_jar = None
+
+
class RAMCookieJar(QNetworkCookieJar):
"""An in-RAM cookie jar.
@@ -112,3 +116,10 @@ class CookieJar(RAMCookieJar):
self._lineparser.data = []
self._lineparser.save()
self.changed.emit()
+
+
+def init(qapp):
+ """Initialize the global cookie jars."""
+ global cookie_jar, ram_cookie_jar
+ cookie_jar = CookieJar(qapp)
+ ram_cookie_jar = RAMCookieJar(qapp)
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index bcdc2bfa2..9fd713475 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -32,8 +32,9 @@ from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug)
from qutebrowser.browser import shared
+from qutebrowser.browser.network import proxy as proxymod
from qutebrowser.extensions import interceptors
-from qutebrowser.browser.webkit import certificateerror
+from qutebrowser.browser.webkit import certificateerror, cookies, cache
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
from qutebrowser.misc import objects
@@ -174,9 +175,10 @@ class NetworkManager(QNetworkAccessManager):
def _set_cookiejar(self):
"""Set the cookie jar of the NetworkManager correctly."""
if self._private:
- cookie_jar = objreg.get('ram-cookie-jar')
+ cookie_jar = cookies.ram_cookie_jar
else:
- cookie_jar = objreg.get('cookie-jar')
+ cookie_jar = cookies.cookie_jar
+ assert cookie_jar is not None
# We have a shared cookie jar - we restore its parent so we don't
# take ownership of it.
@@ -191,9 +193,8 @@ class NetworkManager(QNetworkAccessManager):
# We have a shared cache - we restore its parent so we don't take
# ownership of it.
app = QCoreApplication.instance()
- cache = objreg.get('cache')
- self.setCache(cache)
- cache.setParent(app)
+ self.setCache(cache.diskcache)
+ cache.diskcache.setParent(app)
def _get_abort_signals(self, owner=None):
"""Get a list of signals which should abort a question."""
@@ -375,9 +376,8 @@ class NetworkManager(QNetworkAccessManager):
Return:
A QNetworkReply.
"""
- proxy_factory = objreg.get('proxy-factory', None)
- if proxy_factory is not None:
- proxy_error = proxy_factory.get_error()
+ if proxymod.application_factory is not None:
+ proxy_error = proxymod.application_factory.get_error()
if proxy_error is not None:
return networkreply.ErrorNetworkReply(
req, proxy_error, QNetworkReply.UnknownProxyError,
diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py
index 1dc1c7797..952e1c340 100644
--- a/qutebrowser/commands/command.py
+++ b/qutebrowser/commands/command.py
@@ -359,7 +359,8 @@ class Command:
tab_id = None
else:
raise ValueError("Invalid scope {}!".format(scope))
- return objreg.get(name, scope=scope, window=win_id, tab=tab_id)
+ return objreg.get(name, scope=scope, window=win_id, tab=tab_id,
+ from_command=True)
def _add_special_arg(self, *, value, param, args, kwargs):
"""Add a special argument value to a function call.
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index 7462eff51..994f7e71a 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -32,6 +32,7 @@ from qutebrowser.config import config
from qutebrowser.commands import cmdexc
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split, objects
+from qutebrowser.keyinput import macros
if typing.TYPE_CHECKING:
from qutebrowser.mainwindow import tabbedbrowser
@@ -303,7 +304,21 @@ class CommandParser:
return split_args
-class CommandRunner(QObject):
+class AbstractCommandRunner(QObject):
+
+ """Abstract base class for CommandRunner."""
+
+ def run(self, text, count=None, *, safely=False):
+ raise NotImplementedError
+
+ @pyqtSlot(str, int)
+ @pyqtSlot(str)
+ def run_safely(self, text, count=None):
+ """Run a command and display exceptions in the statusbar."""
+ self.run(text, count, safely=True)
+
+
+class CommandRunner(AbstractCommandRunner):
"""Parse and run qutebrowser commandline commands.
@@ -369,11 +384,4 @@ class CommandRunner(QObject):
last_command[cur_mode] = (text, count)
if record_macro and cur_mode == usertypes.KeyMode.normal:
- macro_recorder = objreg.get('macro-recorder')
- macro_recorder.record_command(text, count)
-
- @pyqtSlot(str, int)
- @pyqtSlot(str)
- def run_safely(self, text, count=None):
- """Run a command and display exceptions in the statusbar."""
- self.run(text, count, safely=True)
+ macros.macro_recorder.record_command(text, count)
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index 608d396f2..f5ce706f8 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -25,14 +25,15 @@ subclasses to provide completions.
import typing
-
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
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.utils import utils, usertypes, debug, log
from qutebrowser.api import cmdutils
+if typing.TYPE_CHECKING:
+ from qutebrowser.mainwindow.statusbar import command
class CompletionView(QTreeView):
@@ -50,6 +51,7 @@ class CompletionView(QTreeView):
_delegate: The item delegate used.
_column_widths: A list of column widths, in percent.
_active: Whether a selection is active.
+ _cmd: The statusbar Command object.
Signals:
update_geometry: Emitted when the completion should be resized.
@@ -108,14 +110,18 @@ class CompletionView(QTreeView):
update_geometry = pyqtSignal()
selection_changed = pyqtSignal(str)
- def __init__(self, win_id: int, parent: QWidget = None) -> None:
+ def __init__(self, *,
+ cmd: 'command.Command',
+ win_id: int,
+ parent: QWidget = None) -> None:
super().__init__(parent)
self.pattern = None # type: typing.Optional[str]
self._win_id = win_id
- config.instance.changed.connect(self._on_config_changed)
-
+ self._cmd = cmd
self._active = False
+ config.instance.changed.connect(self._on_config_changed)
+
self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion'))
@@ -243,15 +249,13 @@ class CompletionView(QTreeView):
history: Navigate through command history if no text was typed.
"""
if history:
- status = objreg.get('status-command', scope='window',
- window=self._win_id)
- if (status.text() == ':' or status.history.is_browsing() or
+ if (self._cmd.text() == ':' or self._cmd.history.is_browsing() or
not self._active):
if which == 'next':
- status.command_history_next()
+ self._cmd.command_history_next()
return
elif which == 'prev':
- status.command_history_prev()
+ self._cmd.command_history_prev()
return
else:
raise cmdutils.CommandError("Can't combine --history with "
@@ -412,9 +416,7 @@ class CompletionView(QTreeView):
Args:
sel: Use the primary selection instead of the clipboard.
"""
- status = objreg.get('status-command', scope='window',
- window=self._win_id)
- text = status.selectedText()
+ text = self._cmd.selectedText()
if not text:
index = self.currentIndex()
if not index.isValid():
diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py
index f48d73206..9cfe22656 100644
--- a/qutebrowser/completion/models/miscmodels.py
+++ b/qutebrowser/completion/models/miscmodels.py
@@ -85,10 +85,11 @@ def bookmark(*, info=None): # pylint: disable=unused-argument
def session(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with session names."""
+ from qutebrowser.misc import sessions
model = completionmodel.CompletionModel()
try:
- manager = objreg.get('session-manager')
- sessions = ((name,) for name in manager.list_sessions()
+ sessions = ((name,) for name
+ in sessions.session_manager.list_sessions()
if not name.startswith('_'))
model.add_category(listcategory.ListCategory("Sessions", sessions))
except OSError:
diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py
index 281047faf..502da10db 100644
--- a/qutebrowser/completion/models/util.py
+++ b/qutebrowser/completion/models/util.py
@@ -21,7 +21,7 @@
import typing
-from qutebrowser.utils import objreg, usertypes
+from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@@ -43,7 +43,7 @@ def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
cmdlist = []
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
for obj in set(objects.commands.values()):
- hide_debug = obj.debug and not objreg.get('args').debug
+ hide_debug = obj.debug and not objects.args.debug
hide_mode = (usertypes.KeyMode.normal not in obj.modes and
not include_hidden)
if not (hide_debug or hide_mode or obj.deprecated):
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index 6cfacbc51..d2e50a645 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -1789,6 +1789,14 @@ tabs.focus_stack_size:
zero_ok: false
desc: Maximum stack size to remember for tab switches (-1 for no maximum).
+tabs.tooltips:
+ default: true
+ type: Bool
+ desc: >-
+ Show tooltips on tabs.
+
+ Note this setting only affects windows opened after it has been set.
+
## url
url.auto_search:
@@ -2768,7 +2776,7 @@ bindings.default:
tIu: config-cycle -p -u {url} content.images ;; reload
insert:
: open-editor
- : insert-text {primary}
+ : insert-text -- {primary}
: leave-mode
hint:
: follow-hint
diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py
index df171c365..d83ca403b 100644
--- a/qutebrowser/config/configexc.py
+++ b/qutebrowser/config/configexc.py
@@ -141,11 +141,13 @@ class ConfigFileErrors(Error):
def __init__(self,
basename: str,
- errors: typing.Sequence[ConfigErrorDesc]) -> None:
+ errors: typing.Sequence[ConfigErrorDesc], *,
+ fatal: bool = False) -> None:
super().__init__("Errors occurred while reading {}:\n{}".format(
basename, '\n'.join(' {}'.format(e) for e in errors)))
self.basename = basename
self.errors = errors
+ self.fatal = fatal
for err in errors:
if err.traceback:
log.config.debug("Config error stack:")
diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py
index 2618d185a..ebfec1354 100644
--- a/qutebrowser/config/configfiles.py
+++ b/qutebrowser/config/configfiles.py
@@ -54,7 +54,6 @@ class StateConfig(configparser.ConfigParser):
super().__init__()
self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8')
-
qt_version = qVersion()
# We handle this here, so we can avoid setting qt_version_changed if
# the config is brand new, but can still set it when qt_version wasn't
@@ -159,7 +158,7 @@ class YamlConfig(QObject):
# Instead, create a config.py - see :help for details.
""".lstrip('\n')))
- utils.yaml_dump(data, f)
+ utils.yaml_dump(data, f) # type: ignore
def _pop_object(self,
yaml_data: typing.Any,
@@ -441,11 +440,21 @@ class ConfigAPI:
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: str,
+ command: typing.Optional[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)
+ if command is None:
+ text = ("Unbinding commands with config.bind('{key}', None) "
+ "is deprecated. Use config.unbind('{key}') instead."
+ .format(key=key))
+ self.errors.append(configexc.ConfigErrorDesc(
+ "While unbinding '{}'".format(key), text))
+ self._keyconfig.unbind(seq, mode=mode)
+ else:
+ self._keyconfig.bind(seq, command, mode=mode)
def unbind(self, key: str, mode: str = 'normal') -> None:
"""Unbind a key from a command, with an optional key mode."""
@@ -679,7 +688,13 @@ def saved_sys_properties() -> typing.Iterator[None]:
def init() -> None:
"""Initialize config storage not related to the main config."""
global state
- state = StateConfig()
+
+ try:
+ state = StateConfig()
+ except configparser.Error as e:
+ msg = "While loading state file from {}".format(standarddir.data())
+ desc = configexc.ConfigErrorDesc(msg, e)
+ raise configexc.ConfigFileErrors('state', [desc], fatal=True)
# Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py
index 74c07e3ab..256c705f0 100644
--- a/qutebrowser/config/configinit.py
+++ b/qutebrowser/config/configinit.py
@@ -57,9 +57,10 @@ def early_init(args: argparse.Namespace) -> None:
config_commands = configcommands.ConfigCommands(
config.instance, config.key_instance)
- objreg.register('config-commands', config_commands)
+ objreg.register('config-commands', config_commands, command_only=True)
config_file = standarddir.config_py()
+ global _init_errors
try:
if os.path.exists(config_file):
@@ -68,10 +69,12 @@ def early_init(args: argparse.Namespace) -> None:
configfiles.read_autoconfig()
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading {}".format(e.basename))
- global _init_errors
_init_errors = e
- configfiles.init()
+ try:
+ configfiles.init()
+ except configexc.ConfigFileErrors as e:
+ _init_errors = e
for opt, val in args.temp_settings:
try:
@@ -149,6 +152,10 @@ def late_init(save_manager: savemanager.SaveManager) -> None:
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
+
+ if _init_errors.fatal:
+ sys.exit(usertypes.Exit.err_init)
+
_init_errors = None
config.instance.init_save_manager(save_manager)
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index f228c2cde..db8f77387 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -22,7 +22,7 @@
import typing
import argparse
-from PyQt5.QtCore import QUrl
+from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtGui import QFont
from qutebrowser.config import config, configutils
@@ -198,6 +198,7 @@ def init(args: argparse.Namespace) -> None:
pattern=urlmatch.UrlPattern(pattern))
+@pyqtSlot()
def shutdown() -> None:
"""Shut down QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine:
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 3becd86bb..3094871c8 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -32,7 +32,8 @@ from PyQt5.QtCore import pyqtSlot
from qutebrowser import components
from qutebrowser.config import config
-from qutebrowser.utils import log, standarddir, objreg
+from qutebrowser.utils import log, standarddir
+from qutebrowser.misc import objects
if typing.TYPE_CHECKING:
import argparse
@@ -137,7 +138,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'))
+ args=objects.args)
def _load_component(info: ExtensionInfo, *,
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 6dd003e18..14dc0608b 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -42,7 +42,7 @@ class MatchResult:
command = attr.ib() # type: typing.Optional[str]
sequence = attr.ib() # type: keyutils.KeySequence
- def __attrs_post_init__(self):
+ def __attrs_post_init__(self) -> None:
if self.match_type == QKeySequence.ExactMatch:
assert self.command is not None
else:
@@ -151,13 +151,13 @@ class BaseKeyParser(QObject):
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
+ supports_count: Whether count is supported.
Attributes:
bindings: Bound key bindings
_win_id: The window ID this keyparser is associated with.
_sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
- _supports_count: Whether count is supported
Signals:
keystring_updated: Emitted when the keystring is updated.
@@ -172,20 +172,19 @@ class BaseKeyParser(QObject):
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
do_log = True
passthrough = False
+ supports_count = True
- def __init__(self, win_id: int, parent: QWidget = None,
- supports_count: bool = True) -> None:
+ def __init__(self, win_id: int, parent: QWidget = None) -> None:
super().__init__(parent)
self._win_id = win_id
self._modename = None
self._sequence = keyutils.KeySequence()
self._count = ''
- self._supports_count = supports_count
self.bindings = BindingTrie()
config.instance.changed.connect(self._on_config_changed)
def __repr__(self) -> str:
- return utils.get_repr(self, supports_count=self._supports_count)
+ return utils.get_repr(self)
def _debug_log(self, message: str) -> None:
"""Log a message to the debug log if logging is active.
@@ -237,7 +236,7 @@ class BaseKeyParser(QObject):
dry_run: bool) -> bool:
"""Try to match a key as count."""
txt = str(sequence[-1]) # To account for sequences changed above.
- if (txt in string.digits and self._supports_count and
+ if (txt in string.digits and self.supports_count and
not (not self._count and txt == '0')):
self._debug_log("Trying match as count")
assert len(txt) == 1, txt
@@ -281,6 +280,8 @@ class BaseKeyParser(QObject):
return QKeySequence.NoMatch
result = self._match_key(sequence)
+ del sequence # Enforce code below to use the modified result.sequence
+
if result.match_type == QKeySequence.NoMatch:
result = self._match_without_modifiers(result.sequence)
if result.match_type == QKeySequence.NoMatch:
@@ -305,7 +306,7 @@ class BaseKeyParser(QObject):
elif result.match_type == QKeySequence.PartialMatch:
self._debug_log("No match for '{}' (added {})".format(
result.sequence, txt))
- self.keystring_updated.emit(self._count + str(sequence))
+ self.keystring_updated.emit(self._count + str(result.sequence))
elif result.match_type == QKeySequence.NoMatch:
self._debug_log("Giving up with '{}', no matches".format(
result.sequence))
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
index 9e12e1a88..b492e8c0a 100644
--- a/qutebrowser/keyinput/keyutils.py
+++ b/qutebrowser/keyinput/keyutils.py
@@ -103,18 +103,18 @@ def _build_special_names() -> typing.Mapping[Qt.Key, str]:
'Dead_Hook': 'Hook',
'Dead_Horn': 'Horn',
- 'Dead_Stroke': '̵',
- 'Dead_Abovecomma': '̓',
- 'Dead_Abovereversedcomma': '̔',
- 'Dead_Doublegrave': '̏',
- 'Dead_Belowring': '̥',
- 'Dead_Belowmacron': '̱',
- 'Dead_Belowcircumflex': '̭',
- 'Dead_Belowtilde': '̰',
- 'Dead_Belowbreve': '̮',
- 'Dead_Belowdiaeresis': '̤',
- 'Dead_Invertedbreve': '̑',
- 'Dead_Belowcomma': '̦',
+ 'Dead_Stroke': '\u0335', # '̵'
+ 'Dead_Abovecomma': '\u0313', # '̓'
+ 'Dead_Abovereversedcomma': '\u0314', # '̔'
+ 'Dead_Doublegrave': '\u030f', # '̏'
+ 'Dead_Belowring': '\u0325', # '̥'
+ 'Dead_Belowmacron': '\u0331', # '̱'
+ 'Dead_Belowcircumflex': '\u032d', # '̭'
+ 'Dead_Belowtilde': '\u0330', # '̰'
+ 'Dead_Belowbreve': '\u032e', # '̮'
+ 'Dead_Belowdiaeresis': '\u0324', # '̤'
+ 'Dead_Invertedbreve': '\u0311', # '̑'
+ 'Dead_Belowcomma': '\u0326', # '̦'
'Dead_Currency': '¤',
'Dead_a': 'a',
'Dead_A': 'A',
@@ -129,10 +129,10 @@ def _build_special_names() -> typing.Mapping[Qt.Key, str]:
'Dead_Small_Schwa': 'ə',
'Dead_Capital_Schwa': 'Ə',
'Dead_Greek': 'Greek',
- 'Dead_Lowline': '̲',
- 'Dead_Aboveverticalline': '̍',
+ 'Dead_Lowline': '\u0332', # '̲'
+ 'Dead_Aboveverticalline': '\u030d', # '̍'
'Dead_Belowverticalline': '\u0329',
- 'Dead_Longsolidusoverlay': '̸',
+ 'Dead_Longsolidusoverlay': '\u0338', # '̸'
'Memo': 'Memo',
'ToDoList': 'To Do List',
@@ -554,7 +554,9 @@ class KeySequence:
def __getitem__(self, item: slice) -> 'KeySequence':
...
- def __getitem__(self, item):
+ def __getitem__(
+ self, item: typing.Union[int, slice]
+ ) -> typing.Union[KeyInfo, 'KeySequence']:
if isinstance(item, slice):
keys = list(self._iter_keys())
return self.__class__(*keys[item])
diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py
index a0116c19f..dd12a4b8f 100644
--- a/qutebrowser/keyinput/macros.py
+++ b/qutebrowser/keyinput/macros.py
@@ -20,12 +20,19 @@
"""Keyboard macro system."""
+import typing
+
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, objreg, usertypes
+_CommandType = typing.Tuple[str, int] # command, type
+
+macro_recorder = typing.cast('MacroRecorder', None)
+
+
class MacroRecorder:
"""An object for recording and running keyboard macros.
@@ -39,15 +46,15 @@ class MacroRecorder:
_last_register: The macro which did run last.
"""
- def __init__(self):
- self._macros = {}
- self._recording_macro = None
- self._macro_count = {}
- self._last_register = None
+ def __init__(self) -> None:
+ self._macros = {} # type: typing.Dict[str, typing.List[_CommandType]]
+ self._recording_macro = None # type: typing.Optional[str]
+ self._macro_count = {} # type: typing.Dict[int, int]
+ self._last_register = None # type: typing.Optional[str]
@cmdutils.register(instance='macro-recorder', name='record-macro')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
- def record_macro_command(self, win_id, register=None):
+ def record_macro_command(self, win_id: int, register: str = None) -> None:
"""Start or stop recording a macro.
Args:
@@ -64,7 +71,7 @@ class MacroRecorder:
message.info("Macro '{}' recorded.".format(self._recording_macro))
self._recording_macro = None
- def record_macro(self, register):
+ def record_macro(self, register: str) -> None:
"""Start recording a macro."""
message.info("Recording macro '{}'...".format(register))
self._macros[register] = []
@@ -73,7 +80,9 @@ class MacroRecorder:
@cmdutils.register(instance='macro-recorder', name='run-macro')
@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):
+ def run_macro_command(self, win_id: int,
+ count: int = 1,
+ register: str = None) -> None:
"""Run a recorded macro.
Args:
@@ -87,7 +96,7 @@ class MacroRecorder:
else:
self.run_macro(win_id, register)
- def run_macro(self, win_id, register):
+ def run_macro(self, win_id: int, register: str) -> None:
"""Run a recorded macro."""
if register == '@':
if self._last_register is None:
@@ -104,13 +113,14 @@ class MacroRecorder:
for cmd in self._macros[register]:
commandrunner.run_safely(*cmd)
- def record_command(self, text, count):
+ def record_command(self, text: str, count: int) -> None:
"""Record a command if a macro is being recorded."""
if self._recording_macro is not None:
self._macros[self._recording_macro].append((text, count))
-def init():
+def init() -> None:
"""Initialize the MacroRecorder."""
+ global macro_recorder
macro_recorder = MacroRecorder()
- objreg.register('macro-recorder', macro_recorder)
+ objreg.register('macro-recorder', macro_recorder, command_only=True)
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index f4b2bf9ac..9e50481c7 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -20,19 +20,26 @@
"""Mode manager singleton which handles the current keyboard mode."""
import functools
+import typing
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
+from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
-from qutebrowser.keyinput import modeparsers
+from qutebrowser.commands import runners
+from qutebrowser.keyinput import modeparsers, basekeyparser
from qutebrowser.config import config
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
+from qutebrowser.browser import hints
INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough]
PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]
+_ParserDictType = typing.MutableMapping[
+ usertypes.KeyMode, basekeyparser.BaseKeyParser]
+
@attr.s(frozen=True)
class KeyEvent:
@@ -48,13 +55,13 @@ class KeyEvent:
text: A string (QKeyEvent::text).
"""
- key = attr.ib()
- text = attr.ib()
+ key = attr.ib() # type: Qt.Key
+ text = attr.ib() # type: str
@classmethod
- def from_event(cls, event):
+ def from_event(cls, event: QKeyEvent) -> 'KeyEvent':
"""Initialize a KeyEvent from a QKeyEvent."""
- return cls(event.key(), event.text())
+ return cls(Qt.Key(event.key()), event.text())
class NotInModeError(Exception):
@@ -62,57 +69,129 @@ class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in."""
-def init(win_id, parent):
+def init(win_id: int, parent: QObject) -> 'ModeManager':
"""Initialize the mode manager and the keyparsers for the given win_id."""
- KM = usertypes.KeyMode # noqa: N806
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
+
+ commandrunner = runners.CommandRunner(win_id)
+
+ hintmanager = hints.HintManager(win_id, parent=parent)
+ objreg.register('hintmanager', hintmanager, scope='window',
+ window=win_id, command_only=True)
+
keyparsers = {
- KM.normal:
- modeparsers.NormalKeyParser(win_id, modeman),
- KM.hint:
- modeparsers.HintKeyParser(win_id, modeman),
- KM.insert:
- modeparsers.PassthroughKeyParser(win_id, 'insert', modeman),
- KM.passthrough:
- modeparsers.PassthroughKeyParser(win_id, 'passthrough', modeman),
- KM.command:
- modeparsers.PassthroughKeyParser(win_id, 'command', modeman),
- KM.prompt:
- modeparsers.PassthroughKeyParser(win_id, 'prompt', modeman),
- KM.yesno:
- modeparsers.PromptKeyParser(win_id, modeman),
- KM.caret:
- modeparsers.CaretKeyParser(win_id, modeman),
- KM.set_mark:
- modeparsers.RegisterKeyParser(win_id, KM.set_mark, modeman),
- KM.jump_mark:
- modeparsers.RegisterKeyParser(win_id, KM.jump_mark, modeman),
- KM.record_macro:
- modeparsers.RegisterKeyParser(win_id, KM.record_macro, modeman),
- KM.run_macro:
- modeparsers.RegisterKeyParser(win_id, KM.run_macro, modeman),
- }
+ usertypes.KeyMode.normal:
+ modeparsers.NormalKeyParser(
+ win_id=win_id,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.hint:
+ modeparsers.HintKeyParser(
+ win_id=win_id,
+ commandrunner=commandrunner,
+ hintmanager=hintmanager,
+ parent=modeman),
+
+ usertypes.KeyMode.insert:
+ modeparsers.PassthroughKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.insert,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.passthrough:
+ modeparsers.PassthroughKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.passthrough,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.command:
+ modeparsers.PassthroughKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.command,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.prompt:
+ modeparsers.PassthroughKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.prompt,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.yesno:
+ modeparsers.PromptKeyParser(
+ win_id=win_id,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.caret:
+ modeparsers.CaretKeyParser(
+ win_id=win_id,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.set_mark:
+ modeparsers.RegisterKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.set_mark,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.jump_mark:
+ modeparsers.RegisterKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.jump_mark,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.record_macro:
+ modeparsers.RegisterKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.record_macro,
+ commandrunner=commandrunner,
+ parent=modeman),
+
+ usertypes.KeyMode.run_macro:
+ modeparsers.RegisterKeyParser(
+ win_id=win_id,
+ mode=usertypes.KeyMode.run_macro,
+ commandrunner=commandrunner,
+ parent=modeman),
+ } # type: _ParserDictType
+
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
- modeman.destroyed.connect(
+
+ modeman.destroyed.connect( # type: ignore
functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id))
+
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
+
return modeman
-def instance(win_id):
+def instance(win_id: int) -> 'ModeManager':
"""Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id)
-def enter(win_id, mode, reason=None, only_if_normal=False):
+def enter(win_id: int,
+ mode: usertypes.KeyMode,
+ reason: str = None,
+ only_if_normal: bool = False) -> None:
"""Enter the mode 'mode'."""
instance(win_id).enter(mode, reason, only_if_normal)
-def leave(win_id, mode, reason=None, *, maybe=False):
+def leave(win_id: int,
+ mode: usertypes.KeyMode,
+ reason: str = None, *,
+ maybe: bool = False) -> None:
"""Leave the mode 'mode'."""
instance(win_id).leave(mode, reason, maybe=maybe)
@@ -144,18 +223,19 @@ class ModeManager(QObject):
entered = pyqtSignal(usertypes.KeyMode, int)
left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int)
- def __init__(self, win_id, parent=None):
+ def __init__(self, win_id: int, parent: QObject = None) -> None:
super().__init__(parent)
self._win_id = win_id
- self._parsers = {}
+ self._parsers = {} # type: _ParserDictType
self._prev_mode = usertypes.KeyMode.normal
self.mode = usertypes.KeyMode.normal
- self._releaseevents_to_pass = set()
+ self._releaseevents_to_pass = set() # type: typing.Set[KeyEvent]
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, mode=self.mode)
- def _handle_keypress(self, event, *, dry_run=False):
+ def _handle_keypress(self, event: QKeyEvent, *,
+ dry_run: bool = False) -> bool:
"""Handle filtering of KeyPress events.
Args:
@@ -199,7 +279,7 @@ class ModeManager(QObject):
filter_this, focus_widget))
return filter_this
- def _handle_keyrelease(self, event):
+ def _handle_keyrelease(self, event: QKeyEvent) -> bool:
"""Handle filtering of KeyRelease events.
Args:
@@ -219,19 +299,16 @@ class ModeManager(QObject):
log.modes.debug("filter: {}".format(filter_this))
return filter_this
- def register(self, mode, parser):
- """Register a new mode.
-
- Args:
- mode: The name of the mode.
- parser: The KeyParser which should be used.
- """
- assert isinstance(mode, usertypes.KeyMode)
+ def register(self, mode: usertypes.KeyMode,
+ parser: basekeyparser.BaseKeyParser) -> None:
+ """Register a new mode."""
assert parser is not None
self._parsers[mode] = parser
parser.request_leave.connect(self.leave)
- def enter(self, mode, reason=None, only_if_normal=False):
+ def enter(self, mode: usertypes.KeyMode,
+ reason: str = None,
+ only_if_normal: bool = False) -> None:
"""Enter a new mode.
Args:
@@ -239,9 +316,6 @@ class ModeManager(QObject):
reason: Why the mode was entered.
only_if_normal: Only enter the new mode if we're in normal mode.
"""
- if not isinstance(mode, usertypes.KeyMode):
- raise TypeError("Mode {} is no KeyMode member!".format(mode))
-
if mode == usertypes.KeyMode.normal:
self.leave(self.mode, reason='enter normal: {}'.format(reason))
return
@@ -273,7 +347,7 @@ class ModeManager(QObject):
self.entered.emit(mode, self._win_id)
@cmdutils.register(instance='mode-manager', scope='window')
- def enter_mode(self, mode):
+ def enter_mode(self, mode: str) -> None:
"""Enter a key mode.
Args:
@@ -294,7 +368,9 @@ class ModeManager(QObject):
self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str, bool)
- def leave(self, mode, reason=None, maybe=False):
+ def leave(self, mode: usertypes.KeyMode,
+ reason: str = None,
+ maybe: bool = False) -> None:
"""Leave a key mode.
Args:
@@ -324,13 +400,13 @@ class ModeManager(QObject):
@cmdutils.register(instance='mode-manager', name='leave-mode',
not_modes=[usertypes.KeyMode.normal], scope='window')
- def leave_current_mode(self):
+ def leave_current_mode(self) -> None:
"""Leave the mode we're currently in."""
if self.mode == usertypes.KeyMode.normal:
raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current')
- def handle_event(self, event):
+ def handle_event(self, event: QEvent) -> bool:
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
@@ -341,20 +417,16 @@ class ModeManager(QObject):
Return:
True if event should be filtered, False otherwise.
"""
- if self.mode is None:
- # We got events before mode is set, so just pass them through.
- return False
-
handlers = {
QEvent.KeyPress: self._handle_keypress,
QEvent.KeyRelease: self._handle_keyrelease,
QEvent.ShortcutOverride:
functools.partial(self._handle_keypress, dry_run=True),
- }
+ } # type: typing.Mapping[QEvent.Type, typing.Callable[[QEvent], bool]]
handler = handlers[event.type()]
return handler(event)
@cmdutils.register(instance='mode-manager', scope='window')
- def clear_keychain(self):
+ def clear_keychain(self) -> None:
"""Clear the currently entered key chain."""
self._parsers[self.mode].clear_keystring()
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index 84f7ffe76..742798e04 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -23,16 +23,20 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input.
"""
+import typing
import traceback
import enum
-from PyQt5.QtCore import pyqtSlot, Qt
-from PyQt5.QtGui import QKeySequence
+from PyQt5.QtCore import pyqtSlot, Qt, QObject
+from PyQt5.QtGui import QKeySequence, QKeyEvent
-from qutebrowser.commands import runners, cmdexc
+from qutebrowser.browser import hints
+from qutebrowser.commands import cmdexc
from qutebrowser.config import config
-from qutebrowser.keyinput import basekeyparser, keyutils
+from qutebrowser.keyinput import basekeyparser, keyutils, macros
from qutebrowser.utils import usertypes, log, message, objreg, utils
+if typing.TYPE_CHECKING:
+ from qutebrowser.commands import runners
STARTCHARS = ":/?"
@@ -47,11 +51,13 @@ class CommandKeyParser(basekeyparser.BaseKeyParser):
_commandrunner: CommandRunner instance.
"""
- def __init__(self, win_id, parent=None, supports_count=None):
- super().__init__(win_id, parent, supports_count)
- self._commandrunner = runners.CommandRunner(win_id)
+ def __init__(self, win_id: int,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
+ super().__init__(win_id, parent)
+ self._commandrunner = commandrunner
- def execute(self, cmdstr, count=None):
+ def execute(self, cmdstr: str, count: int = None) -> None:
try:
self._commandrunner.run(cmdstr, count)
except cmdexc.Error as e:
@@ -66,8 +72,10 @@ class NormalKeyParser(CommandKeyParser):
_partial_timer: Timer to clear partial keypresses.
"""
- def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=True)
+ def __init__(self, win_id: int,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
+ super().__init__(win_id, commandrunner, parent)
self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
@@ -76,20 +84,12 @@ class NormalKeyParser(CommandKeyParser):
self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
self._inhibited_timer.setSingleShot(True)
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self)
- def handle(self, e, *, dry_run=False):
- """Override to abort if the key is a startchar.
-
- Args:
- e: the KeyPressEvent from Qt.
- dry_run: Don't actually execute anything, only check whether there
- would be a match.
-
- Return:
- A self.Match member.
- """
+ def handle(self, e: QKeyEvent, *,
+ dry_run: bool = False) -> QKeySequence.SequenceMatch:
+ """Override to abort if the key is a startchar."""
txt = e.text().strip()
if self._inhibited:
self._debug_log("Ignoring key '{}', because the normal mode is "
@@ -105,7 +105,7 @@ class NormalKeyParser(CommandKeyParser):
self._partial_timer.start()
return match
- def set_inhibited_timeout(self, timeout):
+ def set_inhibited_timeout(self, timeout: int) -> None:
"""Ignore keypresses for the given duration."""
if timeout != 0:
self._debug_log("Inhibiting the normal mode for {}ms.".format(
@@ -116,7 +116,7 @@ class NormalKeyParser(CommandKeyParser):
self._inhibited_timer.start()
@pyqtSlot()
- def _clear_partial_match(self):
+ def _clear_partial_match(self) -> None:
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._sequence))
@@ -124,7 +124,7 @@ class NormalKeyParser(CommandKeyParser):
self.keystring_updated.emit(str(self._sequence))
@pyqtSlot()
- def _clear_inhibited(self):
+ def _clear_inhibited(self) -> None:
"""Reset inhibition state after a timeout."""
self._debug_log("Releasing inhibition state of normal mode.")
self._inhibited = False
@@ -142,8 +142,12 @@ class PassthroughKeyParser(CommandKeyParser):
do_log = False
passthrough = True
+ supports_count = False
- def __init__(self, win_id, mode, parent=None):
+ def __init__(self, win_id: int,
+ mode: usertypes.KeyMode,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
"""Constructor.
Args:
@@ -151,11 +155,11 @@ class PassthroughKeyParser(CommandKeyParser):
parent: Qt parent.
warn: Whether to warn if an ignored key was bound.
"""
- super().__init__(win_id, parent)
- self._read_config(mode)
+ super().__init__(win_id, commandrunner, parent)
+ self._read_config(mode.name)
self._mode = mode
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, mode=self._mode)
@@ -163,11 +167,15 @@ class PromptKeyParser(CommandKeyParser):
"""KeyParser for yes/no prompts."""
- def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=False)
+ supports_count = False
+
+ def __init__(self, win_id: int,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
+ super().__init__(win_id, commandrunner, parent)
self._read_config('yesno')
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self)
@@ -177,31 +185,27 @@ class HintKeyParser(CommandKeyParser):
Attributes:
_filtertext: The text to filter with.
+ _hintmanager: The HintManager to use.
_last_press: The nature of the last keypress, a LastPress member.
"""
- def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=False)
+ supports_count = False
+
+ def __init__(self, win_id: int,
+ commandrunner: 'runners.CommandRunner',
+ hintmanager: hints.HintManager,
+ parent: QObject = None) -> None:
+ super().__init__(win_id, commandrunner, parent)
+ self._hintmanager = hintmanager
self._filtertext = ''
self._last_press = LastPress.none
self._read_config('hint')
- self.keystring_updated.connect(self.on_keystring_updated)
+ self.keystring_updated.connect(self._hintmanager.handle_partial_key)
- def _handle_filter_key(self, e):
- """Handle keys for string filtering.
-
- Return True if the keypress has been handled, and False if not.
-
- Args:
- e: the KeyPressEvent from Qt.
-
- Return:
- A QKeySequence match.
- """
+ def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
+ """Handle keys for string filtering."""
log.keyboard.debug("Got filter key 0x{:x} text {}".format(
e.key(), e.text()))
- hintmanager = objreg.get('hintmanager', scope='tab',
- window=self._win_id, tab='current')
if e.key() == Qt.Key_Backspace:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
"sequence '{}'".format(self._last_press,
@@ -209,7 +213,7 @@ class HintKeyParser(CommandKeyParser):
self._sequence))
if self._last_press != LastPress.keystring and self._filtertext:
self._filtertext = self._filtertext[:-1]
- hintmanager.filter_hints(self._filtertext)
+ self._hintmanager.filter_hints(self._filtertext)
return QKeySequence.ExactMatch
elif self._last_press == LastPress.keystring and self._sequence:
self._sequence = self._sequence[:-1]
@@ -217,36 +221,28 @@ class HintKeyParser(CommandKeyParser):
if not self._sequence and self._filtertext:
# Switch back to hint filtering mode (this can happen only
# in numeric mode after the number has been deleted).
- hintmanager.filter_hints(self._filtertext)
+ self._hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return QKeySequence.ExactMatch
else:
return QKeySequence.NoMatch
- elif hintmanager.current_mode() != 'number':
+ elif self._hintmanager.current_mode() != 'number':
return QKeySequence.NoMatch
elif not e.text():
return QKeySequence.NoMatch
else:
self._filtertext += e.text()
- hintmanager.filter_hints(self._filtertext)
+ self._hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return QKeySequence.ExactMatch
- def handle(self, e, *, dry_run=False):
- """Handle a new keypress and call the respective handlers.
-
- Args:
- e: the KeyPressEvent from Qt
- dry_run: Don't actually execute anything, only check whether there
- would be a match.
-
- Returns:
- True if the match has been handled, False otherwise.
- """
+ def handle(self, e: QKeyEvent, *,
+ dry_run: bool = False) -> QKeySequence.SequenceMatch:
+ """Handle a new keypress and call the respective handlers."""
if dry_run:
return super().handle(e, dry_run=True)
- if keyutils.is_special_hint_mode(e.key(), e.modifiers()):
+ if keyutils.is_special_hint_mode(Qt.Key(e.key()), e.modifiers()):
log.keyboard.debug("Got special key, clearing keychain")
self.clear_keystring()
@@ -265,7 +261,8 @@ class HintKeyParser(CommandKeyParser):
return match
- def update_bindings(self, strings, preserve_filter=False):
+ def update_bindings(self, strings: typing.Sequence[str],
+ preserve_filter: bool = False) -> None:
"""Update bindings when the hint strings changed.
Args:
@@ -279,13 +276,6 @@ class HintKeyParser(CommandKeyParser):
if not preserve_filter:
self._filtertext = ''
- @pyqtSlot(str)
- def on_keystring_updated(self, keystr):
- """Update hintmanager when the keystring was updated."""
- hintmanager = objreg.get('hintmanager', scope='tab',
- window=self._win_id, tab='current')
- hintmanager.handle_partial_key(keystr)
-
class CaretKeyParser(CommandKeyParser):
@@ -293,8 +283,10 @@ class CaretKeyParser(CommandKeyParser):
passthrough = True
- def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=True)
+ def __init__(self, win_id: int,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
+ super().__init__(win_id, commandrunner, parent)
self._read_config('caret')
@@ -307,27 +299,24 @@ class RegisterKeyParser(CommandKeyParser):
and KeyMode.run_macro.
"""
- def __init__(self, win_id, mode, parent=None):
- super().__init__(win_id, parent, supports_count=False)
+ supports_count = False
+
+ def __init__(self, win_id: int,
+ mode: usertypes.KeyMode,
+ commandrunner: 'runners.CommandRunner',
+ parent: QObject = None) -> None:
+ super().__init__(win_id, commandrunner, parent)
self._mode = mode
self._read_config('register')
- def handle(self, e, *, dry_run=False):
- """Override handle to always match the next key and use the register.
-
- Args:
- e: the KeyPressEvent from Qt.
- dry_run: Don't actually execute anything, only check whether there
- would be a match.
-
- Return:
- True if event has been handled, False otherwise.
- """
+ def handle(self, e: QKeyEvent, *,
+ dry_run: bool = False) -> QKeySequence.SequenceMatch:
+ """Override to always match the next key and use the register."""
match = super().handle(e, dry_run=dry_run)
if match or dry_run:
return match
- if keyutils.is_special(e.key(), e.modifiers()):
+ if keyutils.is_special(Qt.Key(e.key()), e.modifiers()):
# this is not a proper register key, let it pass and keep going
return QKeySequence.NoMatch
@@ -335,7 +324,6 @@ class RegisterKeyParser(CommandKeyParser):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
- macro_recorder = objreg.get('macro-recorder')
try:
if self._mode == usertypes.KeyMode.set_mark:
@@ -343,9 +331,9 @@ class RegisterKeyParser(CommandKeyParser):
elif self._mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key)
elif self._mode == usertypes.KeyMode.record_macro:
- macro_recorder.record_macro(key)
+ macros.macro_recorder.record_macro(key)
elif self._mode == usertypes.KeyMode.run_macro:
- macro_recorder.run_macro(self._win_id, key)
+ macros.macro_recorder.run_macro(self._win_id, key)
else:
raise ValueError(
"{} is not a valid register mode".format(self._mode))
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 8d571c7eb..fe0f19ed6 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -37,7 +37,7 @@ from qutebrowser.mainwindow import messageview, prompt
from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman
from qutebrowser.browser import commands, downloadview, hints, downloads
-from qutebrowser.misc import crashsignal, keyhintwidget
+from qutebrowser.misc import crashsignal, keyhintwidget, sessions
win_id_gen = itertools.count(0)
@@ -139,6 +139,7 @@ class MainWindow(QWidget):
tabbed_browser: The TabbedBrowser widget.
state_before_fullscreen: window state before activation of fullscreen.
_downloadview: The DownloadView widget.
+ _download_model: The DownloadModel instance.
_vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance.
_overlays: Widgets shown as overlay for the current webpage.
@@ -193,7 +194,8 @@ class MainWindow(QWidget):
self._vbox.setSpacing(0)
self._init_downloadmanager()
- self._downloadview = downloadview.DownloadView(self.win_id)
+ self._downloadview = downloadview.DownloadView(
+ model=self._download_model)
if config.val.content.private_browsing:
# This setting always trumps what's passed in.
@@ -220,7 +222,7 @@ class MainWindow(QWidget):
self._init_completion()
log.init.debug("Initializing modes...")
- modeman.init(self.win_id, self)
+ modeman.init(win_id=self.win_id, parent=self)
self._commandrunner = runners.CommandRunner(self.win_id,
partial_match=True)
@@ -233,7 +235,7 @@ class MainWindow(QWidget):
self._prompt_container.update_geometry,
centered=True, padding=10)
objreg.register('prompt-container', self._prompt_container,
- scope='window', window=self.win_id)
+ scope='window', window=self.win_id, command_only=True)
self._prompt_container.hide()
self._messageview = messageview.MessageView(parent=self)
@@ -248,7 +250,7 @@ class MainWindow(QWidget):
QTimer.singleShot(0, self._connect_overlay_signals)
config.instance.changed.connect(self._on_config_changed)
- objreg.get("app").new_window.emit(self)
+ QApplication.instance().new_window.emit(self)
self._set_decoration(config.val.window.hide_decoration)
self.state_before_fullscreen = self.windowState()
@@ -329,27 +331,32 @@ class MainWindow(QWidget):
except KeyError:
webengine_download_manager = None
- download_model = downloads.DownloadModel(qtnetwork_download_manager,
- webengine_download_manager)
- objreg.register('download-model', download_model, scope='window',
- window=self.win_id)
+ self._download_model = downloads.DownloadModel(
+ qtnetwork_download_manager, webengine_download_manager)
+ objreg.register('download-model', self._download_model,
+ scope='window', window=self.win_id,
+ command_only=True)
def _init_completion(self):
- self._completion = completionwidget.CompletionView(self.win_id, self)
- cmd = objreg.get('status-command', scope='window', window=self.win_id)
- completer_obj = completer.Completer(cmd=cmd, win_id=self.win_id,
+ self._completion = completionwidget.CompletionView(cmd=self.status.cmd,
+ win_id=self.win_id,
+ parent=self)
+ completer_obj = completer.Completer(cmd=self.status.cmd,
+ win_id=self.win_id,
parent=self._completion)
self._completion.selection_changed.connect(
completer_obj.on_selection_changed)
objreg.register('completion', self._completion, scope='window',
- window=self.win_id)
+ window=self.win_id, command_only=True)
self._add_overlay(self._completion, self._completion.update_geometry)
def _init_command_dispatcher(self):
- dispatcher = commands.CommandDispatcher(self.win_id,
- self.tabbed_browser)
- objreg.register('command-dispatcher', dispatcher, scope='window',
- window=self.win_id)
+ self._command_dispatcher = commands.CommandDispatcher(
+ self.win_id, self.tabbed_browser)
+ objreg.register('command-dispatcher',
+ self._command_dispatcher,
+ command_only=True,
+ scope='window', window=self.win_id)
self.tabbed_browser.widget.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=self.win_id))
@@ -445,10 +452,7 @@ class MainWindow(QWidget):
def _connect_signals(self):
"""Connect all mainwindow signals."""
- status = self._get_object('statusbar')
keyparsers = self._get_object('keyparsers')
- completion_obj = self._get_object('completion')
- cmd = self._get_object('status-command')
message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager')
@@ -457,17 +461,22 @@ class MainWindow(QWidget):
mode_manager.entered.connect(hints.on_mode_entered)
# status bar
- mode_manager.entered.connect(status.on_mode_entered)
- mode_manager.left.connect(status.on_mode_left)
- mode_manager.left.connect(cmd.on_mode_left)
+ mode_manager.entered.connect(self.status.on_mode_entered)
+ mode_manager.left.connect(self.status.on_mode_left)
+ mode_manager.left.connect(self.status.cmd.on_mode_left)
mode_manager.left.connect(message.global_bridge.mode_left)
# commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
- status.keystring.setText)
- cmd.got_cmd[str].connect(self._commandrunner.run_safely)
- cmd.got_cmd[str, int].connect(self._commandrunner.run_safely)
- cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed)
+ self.status.keystring.setText)
+ self.status.cmd.got_cmd[str].connect(
+ self._commandrunner.run_safely)
+ self.status.cmd.got_cmd[str, int].connect(
+ self._commandrunner.run_safely)
+ self.status.cmd.returnPressed.connect(
+ self.tabbed_browser.on_cmd_return_pressed)
+ self.status.cmd.got_search.connect(
+ self._command_dispatcher.search)
# key hint popup
for mode, parser in keyparsers.items():
@@ -481,42 +490,51 @@ class MainWindow(QWidget):
message.global_bridge.clear_messages.connect(
self._messageview.clear_messages)
- message_bridge.s_set_text.connect(status.set_text)
- message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
+ message_bridge.s_set_text.connect(self.status.set_text)
+ message_bridge.s_maybe_reset_text.connect(
+ self.status.txt.maybe_reset_text)
# statusbar
- self.tabbed_browser.current_tab_changed.connect(status.on_tab_changed)
+ self.tabbed_browser.current_tab_changed.connect(
+ self.status.on_tab_changed)
- self.tabbed_browser.cur_progress.connect(status.prog.on_load_progress)
+ self.tabbed_browser.cur_progress.connect(
+ self.status.prog.on_load_progress)
self.tabbed_browser.cur_load_started.connect(
- status.prog.on_load_started)
+ self.status.prog.on_load_started)
self.tabbed_browser.cur_scroll_perc_changed.connect(
- status.percentage.set_perc)
+ self.status.percentage.set_perc)
self.tabbed_browser.widget.tab_index_changed.connect(
- status.tabindex.on_tab_index_changed)
+ self.status.tabindex.on_tab_index_changed)
- self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
+ self.tabbed_browser.cur_url_changed.connect(
+ self.status.url.set_url)
self.tabbed_browser.cur_url_changed.connect(functools.partial(
- status.backforward.on_tab_cur_url_changed,
+ self.status.backforward.on_tab_cur_url_changed,
tabs=self.tabbed_browser))
- self.tabbed_browser.cur_link_hovered.connect(status.url.set_hover_url)
+ self.tabbed_browser.cur_link_hovered.connect(
+ self.status.url.set_hover_url)
self.tabbed_browser.cur_load_status_changed.connect(
- status.url.on_load_status_changed)
+ self.status.url.on_load_status_changed)
self.tabbed_browser.cur_caret_selection_toggled.connect(
- status.on_caret_selection_toggled)
+ self.status.on_caret_selection_toggled)
self.tabbed_browser.cur_fullscreen_requested.connect(
self._on_fullscreen_requested)
- self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide)
+ self.tabbed_browser.cur_fullscreen_requested.connect(
+ self.status.maybe_hide)
# command input / completion
- mode_manager.entered.connect(self.tabbed_browser.on_mode_entered)
- mode_manager.left.connect(self.tabbed_browser.on_mode_left)
- cmd.clear_completion_selection.connect(
- completion_obj.on_clear_completion_selection)
- cmd.hide_completion.connect(completion_obj.hide)
+ mode_manager.entered.connect(
+ self.tabbed_browser.on_mode_entered)
+ mode_manager.left.connect(
+ self.tabbed_browser.on_mode_left)
+ self.status.cmd.clear_completion_selection.connect(
+ self._completion.on_clear_completion_selection)
+ self.status.cmd.hide_completion.connect(
+ self._completion.hide)
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
@@ -579,7 +597,7 @@ class MainWindow(QWidget):
objreg.delete('last-visible-main-window')
except KeyError:
pass
- objreg.get('session-manager').save_last_window_session()
+ sessions.session_manager.save_last_window_session()
self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown()
@@ -590,9 +608,7 @@ class MainWindow(QWidget):
e.accept()
return
tab_count = self.tabbed_browser.widget.count()
- download_model = objreg.get('download-model', scope='window',
- window=self.win_id)
- download_count = download_model.running_downloads()
+ download_count = self._download_model.running_downloads()
quit_texts = []
# Ask if multiple-tabs are open
if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 623e3ecad..81538a68a 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -23,6 +23,7 @@ import os.path
import html
import collections
import functools
+import typing
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
@@ -39,7 +40,7 @@ from qutebrowser.api import cmdutils
from qutebrowser.utils import urlmatch
-prompt_queue = None
+prompt_queue = typing.cast('PromptQueue', None)
@attr.s
@@ -949,6 +950,5 @@ def init():
"""Initialize global prompt objects."""
global prompt_queue
prompt_queue = PromptQueue()
- objreg.register('prompt-queue', prompt_queue) # for commands
message.global_bridge.ask_question.connect(
prompt_queue.ask_question, Qt.DirectConnection)
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index 42b3f4652..52e09cd70 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -157,7 +157,6 @@ class StatusBar(QWidget):
def __init__(self, *, win_id, private, parent=None):
super().__init__(parent)
- objreg.register('statusbar', self, scope='window', window=win_id)
self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground)
config.set_register_stylesheet(self)
diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py
index bdac24d61..5d0d73a61 100644
--- a/qutebrowser/mainwindow/statusbar/command.py
+++ b/qutebrowser/mainwindow/statusbar/command.py
@@ -19,10 +19,10 @@
"""The commandline in the statusbar."""
-import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
-from PyQt5.QtWidgets import QSizePolicy
+from PyQt5.QtGui import QKeyEvent
+from PyQt5.QtWidgets import QSizePolicy, QWidget
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.api import cmdutils
@@ -42,6 +42,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals:
got_cmd: Emitted when a command is triggered by the user.
arg: The command string and also potentially the count.
+ got_search: Emitted when a search should happen.
clear_completion_selection: Emitted before the completion widget is
hidden.
hide_completion: Emitted when the completion widget should be hidden.
@@ -51,13 +52,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""
got_cmd = pyqtSignal([str], [str, int])
+ got_search = pyqtSignal(str, bool) # text, reverse
clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal()
update_completion = pyqtSignal()
show_cmd = pyqtSignal()
hide_cmd = pyqtSignal()
- def __init__(self, *, win_id, private, parent=None):
+ def __init__(self, *, win_id: int,
+ private: bool,
+ parent: QWidget = None) -> None:
misc.CommandLineEdit.__init__(self, parent=parent)
misc.MinimalLineEditMixin.__init__(self)
self._win_id = win_id
@@ -66,32 +70,29 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.history.history = command_history.data
self.history.changed.connect(command_history.changed)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
- self.cursorPositionChanged.connect(self.update_completion)
- self.textChanged.connect(self.update_completion)
+
+ self.cursorPositionChanged.connect(
+ self.update_completion) # type: ignore
+ self.textChanged.connect(self.update_completion) # type: ignore
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
- self._command_dispatcher = objreg.get(
- 'command-dispatcher', scope='window', window=self._win_id)
-
- def _handle_search(self):
+ def _handle_search(self) -> bool:
"""Check if the currently entered text is a search, and if so, run it.
Return:
True if a search was executed, False otherwise.
"""
- search_prefixes = {
- '/': self._command_dispatcher.search,
- '?': functools.partial(
- self._command_dispatcher.search, reverse=True)
- }
- if self.prefix() in search_prefixes:
- search_fn = search_prefixes[self.prefix()]
- search_fn(self.text()[1:])
+ if self.prefix() == '/':
+ self.got_search.emit(self.text()[1:], False)
return True
- return False
+ elif self.prefix() == '?':
+ self.got_search.emit(self.text()[1:], True)
+ return True
+ else:
+ return False
- def prefix(self):
+ def prefix(self) -> str:
"""Get the currently entered command prefix."""
text = self.text()
if not text:
@@ -101,7 +102,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
else:
return ''
- def set_cmd_text(self, text):
+ def set_cmd_text(self, text: str) -> None:
"""Preset the statusbar to some text.
Args:
@@ -116,8 +117,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0)
@cmdutils.argument('count', value=cmdutils.Value.count)
- def set_cmd_text_command(self, text, count=None, space=False, append=False,
- run_on_count=False):
+ def set_cmd_text_command(self, text: str,
+ count: int = None,
+ space: bool = False,
+ append: bool = False,
+ run_on_count: bool = False) -> None:
"""Preset the statusbar to some text.
//
@@ -144,13 +148,13 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
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)
+ self.got_cmd[str, int].emit(text, count) # type: ignore
else:
self.set_cmd_text(text)
@cmdutils.register(instance='status-command',
modes=[usertypes.KeyMode.command], scope='window')
- def command_history_prev(self):
+ def command_history_prev(self) -> None:
"""Go back in the commandline history."""
try:
if not self.history.is_browsing():
@@ -165,7 +169,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command',
modes=[usertypes.KeyMode.command], scope='window')
- def command_history_next(self):
+ def command_history_next(self) -> None:
"""Go forward in the commandline history."""
if not self.history.is_browsing():
return
@@ -178,7 +182,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command',
modes=[usertypes.KeyMode.command], scope='window')
- def command_accept(self, rapid=False):
+ def command_accept(self, rapid: bool = False) -> None:
"""Execute the command currently in the commandline.
Args:
@@ -194,10 +198,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
'cmd accept')
if not was_search:
- self.got_cmd[str].emit(text[1:])
+ self.got_cmd[str].emit(text[1:]) # type: ignore
@cmdutils.register(instance='status-command', scope='window')
- def edit_command(self, run=False):
+ def edit_command(self, run: bool = False) -> None:
"""Open an editor to modify the current command.
Args:
@@ -205,7 +209,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"""
ed = editor.ExternalEditor(parent=self)
- def callback(text):
+ def callback(text: str) -> None:
"""Set the commandline to the edited text."""
if not text or text[0] not in modeparsers.STARTCHARS:
message.error('command must start with one of {}'
@@ -219,7 +223,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
ed.edit(self.text())
@pyqtSlot(usertypes.KeyMode)
- def on_mode_left(self, mode):
+ def on_mode_left(self, mode: usertypes.KeyMode) -> None:
"""Clear up when command mode was left.
- Clear the statusbar text if it's explicitly unfocused.
@@ -236,7 +240,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.clear_completion_selection.emit()
self.hide_completion.emit()
- def setText(self, text):
+ def setText(self, text: str) -> None:
"""Extend setText to set prefix and make sure the prompt is ok."""
if not text:
pass
@@ -247,7 +251,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
"'{}'!".format(text))
super().setText(text)
- def keyPressEvent(self, e):
+ def keyPressEvent(self, e: QKeyEvent) -> None:
"""Override keyPressEvent to ignore Return key presses.
If this widget is focused, we are in passthrough key mode, and
@@ -266,7 +270,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
else:
super().keyPressEvent(e)
- def sizeHint(self):
+ def sizeHint(self) -> QSize:
"""Dynamically calculate the needed size."""
height = super().sizeHint().height()
text = self.text()
@@ -276,7 +280,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
return QSize(width, height)
@pyqtSlot()
- def _incremental_search(self):
+ def _incremental_search(self) -> None:
if not config.val.search.incremental:
return
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index 1953c2012..43b194159 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -153,8 +153,9 @@ class TabWidget(QTabWidget):
if tabbar.tabText(idx) != title:
tabbar.setTabText(idx, title)
- # always show only plain title in tooltips
- tabbar.setTabToolTip(idx, fields['current_title'])
+ if config.cache['tabs.tooltips']:
+ # always show only plain title in tooltips
+ tabbar.setTabToolTip(idx, fields['current_title'])
def get_tab_fields(self, idx):
"""Get the tab field data."""
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py
index 812c058ff..a8d950767 100644
--- a/qutebrowser/misc/backendproblem.py
+++ b/qutebrowser/misc/backendproblem.py
@@ -27,17 +27,21 @@ import ctypes
import ctypes.util
import enum
import shutil
+import typing
+import argparse
import attr
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout,
- QVBoxLayout, QLabel, QMessageBox)
+ QVBoxLayout, QLabel, QMessageBox, QWidget)
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.config import config, configfiles
-from qutebrowser.utils import (usertypes, objreg, version, qtutils, log, utils,
+from qutebrowser.utils import (usertypes, version, qtutils, log, utils,
standarddir)
-from qutebrowser.misc import objects, msgbox
+from qutebrowser.misc import objects, msgbox, savemanager
+if typing.TYPE_CHECKING:
+ from qutebrowser import app
class _Result(enum.IntEnum):
@@ -55,13 +59,15 @@ class _Button:
"""A button passed to BackendProblemDialog."""
- text = attr.ib()
- setting = attr.ib()
- value = attr.ib()
- default = attr.ib(default=False)
+ text = attr.ib() # type: str
+ setting = attr.ib() # type: str
+ value = attr.ib() # type: str
+ default = attr.ib(default=False) # type: bool
-def _other_backend(backend):
+def _other_backend(
+ backend: usertypes.Backend
+) -> typing.Tuple[usertypes.Backend, str]:
"""Get the other backend enum/setting for a given backend."""
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
@@ -71,7 +77,7 @@ def _other_backend(backend):
return (other_backend, other_setting)
-def _error_text(because, text, backend):
+def _error_text(because: str, text: str, backend: usertypes.Backend) -> str:
"""Get an error text for the given information."""
other_backend, other_setting = _other_backend(backend)
if other_backend == usertypes.Backend.QtWebKit:
@@ -96,14 +102,19 @@ class _Dialog(QDialog):
"""A dialog which gets shown if there are issues with the backend."""
- def __init__(self, because, text, backend, buttons=None, parent=None):
+ def __init__(self, *, because: str,
+ text: str,
+ backend: usertypes.Backend,
+ buttons: typing.Sequence[_Button] = None,
+ parent: QWidget = None) -> None:
super().__init__(parent)
vbox = QVBoxLayout(self)
other_backend, other_setting = _other_backend(backend)
text = _error_text(because, text, backend)
- label = QLabel(text, wordWrap=True)
+ label = QLabel(text)
+ label.setWordWrap(True)
label.setTextFormat(Qt.RichText)
vbox.addWidget(label)
@@ -123,18 +134,17 @@ class _Dialog(QDialog):
hbox.addWidget(backend_button)
for button in buttons:
- btn = QPushButton(button.text, default=button.default)
+ btn = QPushButton(button.text)
+ btn.setDefault(button.default)
btn.clicked.connect(functools.partial(
self._change_setting, button.setting, button.value))
hbox.addWidget(btn)
vbox.addLayout(hbox)
- def _change_setting(self, setting, value):
+ def _change_setting(self, setting: str, value: str) -> None:
"""Change the given setting and restart."""
config.instance.set_obj(setting, value, save_yaml=True)
- save_manager = objreg.get('save-manager')
- save_manager.save_all(is_exit=True)
if setting == 'backend' and value == 'webkit':
self.done(_Result.restart_webkit)
@@ -144,296 +154,318 @@ class _Dialog(QDialog):
self.done(_Result.restart)
-def _show_dialog(*args, **kwargs):
- """Show a dialog for a backend problem."""
- cmd_args = objreg.get('args')
- if cmd_args.no_err_windows:
- text = _error_text(*args, **kwargs)
- print(text, file=sys.stderr)
- sys.exit(usertypes.Exit.err_init)
-
- dialog = _Dialog(*args, **kwargs)
-
- status = dialog.exec_()
- quitter = objreg.get('quitter')
-
- if status in [_Result.quit, QDialog.Rejected]:
- pass
- elif status == _Result.restart_webkit:
- quitter.restart(override_args={'backend': 'webkit'})
- elif status == _Result.restart_webengine:
- quitter.restart(override_args={'backend': 'webengine'})
- elif status == _Result.restart:
- quitter.restart()
- else:
- raise utils.Unreachable(status)
-
- sys.exit(usertypes.Exit.err_init)
-
-
-def _nvidia_shader_workaround():
- """Work around QOpenGLShaderProgram issues.
-
- NOTE: This needs to be called before _handle_nouveau_graphics, or some
- setups will segfault in version.opengl_vendor().
-
- See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
- """
- assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
-
- if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
- return
-
- libgl = ctypes.util.find_library("GL")
- if libgl is not None:
- ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
-
-
-def _handle_nouveau_graphics():
- """Force software rendering when using the Nouveau driver.
-
- WORKAROUND for https://bugreports.qt.io/browse/QTBUG-41242
- Should be fixed in Qt 5.10 via https://codereview.qt-project.org/#/c/208664/
- """
- assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
-
- if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'):
- return
-
- if qtutils.version_check('5.10', compiled=False):
- return
-
- if version.opengl_vendor() != 'nouveau':
- return
-
- if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
- # qt.force_software_rendering = 'software-opengl'
- 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or
- # qt.force_software_rendering = 'chromium', also see:
- # https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1
- 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ):
- return
-
- button = _Button("Force software rendering", 'qt.force_software_rendering',
- 'chromium')
- _show_dialog(
- backend=usertypes.Backend.QtWebEngine,
- because="you're using Nouveau graphics",
- text="There are two ways to fix this:
"
- "Forcing software rendering
"
- "This allows you to use the newer QtWebEngine backend (based "
- "on Chromium) but could have noticeable performance impact "
- "(depending on your hardware). "
- "This sets the qt.force_software_rendering = 'chromium' "
- "option (if you have a config.py file, you'll need to set "
- "this manually).
",
- buttons=[button],
- )
-
- raise utils.Unreachable
-
-
-def _handle_wayland():
- assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
-
- if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
- return
-
- platform = QApplication.instance().platformName()
- if platform not in ['wayland', 'wayland-egl']:
- return
-
- has_qt511 = qtutils.version_check('5.11', compiled=False)
- 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:
"
-
- if 'DISPLAY' in os.environ:
- # XWayland is available, but QT_QPA_PLATFORM=wayland is set
- buttons.append(_Button("Force XWayland", 'qt.force_platform', 'xcb'))
- text += ("Force Qt to use XWayland
"
- "This allows you to use the newer QtWebEngine backend "
- "(based on Chromium). "
- "This sets the qt.force_platform = 'xcb' option "
- "(if you have a config.py file, you'll need to set "
- "this manually).
")
- else:
- text += ("Set up XWayland
"
- "This allows you to use the newer QtWebEngine backend "
- "(based on Chromium). ")
-
- if has_qt511:
- buttons.append(_Button("Force software rendering",
- 'qt.force_software_rendering',
- 'chromium'))
- text += ("
Forcing software rendering
"
- "This allows you to use the newer QtWebEngine backend "
- "(based on Chromium) but could have noticeable performance "
- "impact (depending on your hardware). This sets the "
- "qt.force_software_rendering = 'chromium' option "
- "(if you have a config.py file, you'll need to set "
- "this manually).
")
-
- _show_dialog(backend=usertypes.Backend.QtWebEngine,
- because="you're using Wayland", text=text, buttons=buttons)
-
-
@attr.s
-class BackendImports:
+class _BackendImports:
"""Whether backend modules could be imported."""
- webkit_available = attr.ib(default=None)
- webengine_available = attr.ib(default=None)
- webkit_error = attr.ib(default=None)
- webengine_error = attr.ib(default=None)
+ webkit_available = attr.ib(default=None) # type: bool
+ webengine_available = attr.ib(default=None) # type: bool
+ webkit_error = attr.ib(default=None) # type: str
+ webengine_error = attr.ib(default=None) # type: str
-def _try_import_backends():
- """Check whether backends can be imported and return BackendImports."""
- # pylint: disable=unused-import
- results = BackendImports()
+class _BackendProblemChecker:
- try:
- from PyQt5 import QtWebKit
- from PyQt5 import QtWebKitWidgets
- except ImportError as e:
- results.webkit_available = False
- results.webkit_error = str(e)
- else:
- if qtutils.is_new_qtwebkit():
- results.webkit_available = True
+ """Check for various backend-specific issues."""
+
+ def __init__(self, *,
+ quitter: 'app.Quitter',
+ no_err_windows: bool,
+ save_manager: savemanager.SaveManager) -> None:
+ self._quitter = quitter
+ self._save_manager = save_manager
+ self._no_err_windows = no_err_windows
+
+ def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None:
+ """Show a dialog for a backend problem."""
+ if self._no_err_windows:
+ text = _error_text(*args, **kwargs)
+ print(text, file=sys.stderr)
+ sys.exit(usertypes.Exit.err_init)
+
+ dialog = _Dialog(*args, **kwargs)
+
+ status = dialog.exec_()
+ self._save_manager.save_all(is_exit=True)
+
+ if status in [_Result.quit, QDialog.Rejected]:
+ pass
+ elif status == _Result.restart_webkit:
+ self._quitter.restart(override_args={'backend': 'webkit'})
+ elif status == _Result.restart_webengine:
+ self._quitter.restart(override_args={'backend': 'webengine'})
+ elif status == _Result.restart:
+ self._quitter.restart()
else:
- results.webkit_available = False
- results.webkit_error = "Unsupported legacy QtWebKit found"
+ raise utils.Unreachable(status)
- try:
- from PyQt5 import QtWebEngineWidgets
- except ImportError as e:
- results.webengine_available = False
- results.webengine_error = str(e)
- else:
- results.webengine_available = True
-
- assert results.webkit_available is not None
- assert results.webengine_available is not None
- if not results.webkit_available:
- assert results.webkit_error is not None
- if not results.webengine_available:
- assert results.webengine_error is not None
-
- return results
-
-
-def _handle_ssl_support(fatal=False):
- """Check for full SSL availability.
-
- If "fatal" is given, show an error and exit.
- """
- text = ("Could not initialize QtNetwork SSL support. If you use "
- "OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
- "or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
- "of OpenSSL 1.0. This only affects downloads.")
-
- if QSslSocket.supportsSsl():
- return
-
- if fatal:
- errbox = msgbox.msgbox(parent=None,
- title="SSL error",
- text="Could not initialize SSL support.",
- icon=QMessageBox.Critical,
- plain_text=False)
- errbox.exec_()
sys.exit(usertypes.Exit.err_init)
- assert not fatal
- log.init.warning(text)
+ def _nvidia_shader_workaround(self) -> None:
+ """Work around QOpenGLShaderProgram issues.
+ NOTE: This needs to be called before _handle_nouveau_graphics, or some
+ setups will segfault in version.opengl_vendor().
-def _check_backend_modules():
- """Check for the modules needed for QtWebKit/QtWebEngine."""
- imports = _try_import_backends()
+ See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
+ """
+ self._assert_backend(usertypes.Backend.QtWebEngine)
- if imports.webkit_available and imports.webengine_available:
- return
- elif not imports.webkit_available and not imports.webengine_available:
- text = ("qutebrowser needs QtWebKit or QtWebEngine, but neither "
- "could be imported!
"
- "The errors encountered were:
"
- "- QtWebKit: {webkit_error}"
- "
- QtWebEngine: {webengine_error}"
- "
".format(
- webkit_error=html.escape(imports.webkit_error),
- webengine_error=html.escape(imports.webengine_error)))
- errbox = msgbox.msgbox(parent=None,
- title="No backend library found!",
- text=text,
- icon=QMessageBox.Critical,
- plain_text=False)
- errbox.exec_()
- sys.exit(usertypes.Exit.err_init)
- elif objects.backend == usertypes.Backend.QtWebKit:
- if imports.webkit_available:
+ if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
return
- assert imports.webengine_available
- _show_dialog(
- backend=usertypes.Backend.QtWebKit,
- because="QtWebKit could not be imported",
- text="The error encountered was:
{}
".format(
- html.escape(imports.webkit_error))
- )
- elif objects.backend == usertypes.Backend.QtWebEngine:
- if imports.webengine_available:
+
+ libgl = ctypes.util.find_library("GL")
+ if libgl is not None:
+ ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
+
+ def _handle_nouveau_graphics(self) -> None:
+ """Force software rendering when using the Nouveau driver.
+
+ WORKAROUND for
+ https://bugreports.qt.io/browse/QTBUG-41242
+ Should be fixed in Qt 5.10 via
+ https://codereview.qt-project.org/#/c/208664/
+ """
+ self._assert_backend(usertypes.Backend.QtWebEngine)
+
+ if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'):
return
- assert imports.webkit_available
- _show_dialog(
+
+ if qtutils.version_check('5.10', compiled=False):
+ return
+
+ if version.opengl_vendor() != 'nouveau':
+ return
+
+ if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
+ # qt.force_software_rendering = 'software-opengl'
+ 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or
+ # qt.force_software_rendering = 'chromium', also see:
+ # https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1
+ 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ):
+ return
+
+ button = _Button("Force software rendering",
+ 'qt.force_software_rendering',
+ 'chromium')
+ self._show_dialog(
backend=usertypes.Backend.QtWebEngine,
- because="QtWebEngine could not be imported",
- text="The error encountered was:
{}
".format(
- html.escape(imports.webengine_error))
+ because="you're using Nouveau graphics",
+ text=("There are two ways to fix this:
"
+ "Forcing software rendering
"
+ "This allows you to use the newer QtWebEngine backend "
+ "(based on Chromium) but could have noticeable performance "
+ "impact (depending on your hardware). This sets the "
+ "qt.force_software_rendering = 'chromium' option "
+ "(if you have a config.py file, you'll need to set "
+ "this manually).
"),
+ buttons=[button],
)
- raise utils.Unreachable
+ raise utils.Unreachable
+
+ def _handle_wayland(self) -> None:
+ self._assert_backend(usertypes.Backend.QtWebEngine)
+
+ if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
+ return
+
+ platform = QApplication.instance().platformName()
+ if platform not in ['wayland', 'wayland-egl']:
+ return
+
+ has_qt511 = qtutils.version_check('5.11', compiled=False)
+ 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:
"
+
+ if 'DISPLAY' in os.environ:
+ # XWayland is available, but QT_QPA_PLATFORM=wayland is set
+ buttons.append(
+ _Button("Force XWayland", 'qt.force_platform', 'xcb'))
+ text += ("Force Qt to use XWayland
"
+ "This allows you to use the newer QtWebEngine backend "
+ "(based on Chromium). "
+ "This sets the qt.force_platform = 'xcb' option "
+ "(if you have a config.py file, you'll need to "
+ "set this manually).
")
+ else:
+ text += ("Set up XWayland
"
+ "This allows you to use the newer QtWebEngine backend "
+ "(based on Chromium). ")
+
+ if has_qt511:
+ buttons.append(_Button("Force software rendering",
+ 'qt.force_software_rendering',
+ 'chromium'))
+ text += ("
Forcing software rendering
"
+ "This allows you to use the newer QtWebEngine backend "
+ "(based on Chromium) but could have noticeable "
+ "performance impact (depending on your hardware). This "
+ "sets the qt.force_software_rendering = "
+ "'chromium' option (if you have a config.py "
+ "file, you'll need to set this manually).
")
+
+ self._show_dialog(backend=usertypes.Backend.QtWebEngine,
+ because="you're using Wayland",
+ text=text,
+ buttons=buttons)
+
+ def _try_import_backends(self) -> _BackendImports:
+ """Check whether backends can be imported and return BackendImports."""
+ # pylint: disable=unused-import
+ results = _BackendImports()
+
+ try:
+ from PyQt5 import QtWebKit
+ from PyQt5 import QtWebKitWidgets
+ except ImportError as e:
+ results.webkit_available = False
+ results.webkit_error = str(e)
+ else:
+ if qtutils.is_new_qtwebkit():
+ results.webkit_available = True
+ else:
+ results.webkit_available = False
+ results.webkit_error = "Unsupported legacy QtWebKit found"
+
+ try:
+ from PyQt5 import QtWebEngineWidgets
+ except ImportError as e:
+ results.webengine_available = False
+ results.webengine_error = str(e)
+ else:
+ results.webengine_available = True
+
+ assert results.webkit_available is not None
+ assert results.webengine_available is not None
+ if not results.webkit_available:
+ assert results.webkit_error is not None
+ if not results.webengine_available:
+ assert results.webengine_error is not None
+
+ return results
+
+ def _handle_ssl_support(self, fatal: bool = False) -> None:
+ """Check for full SSL availability.
+
+ If "fatal" is given, show an error and exit.
+ """
+ text = ("Could not initialize QtNetwork SSL support. If you use "
+ "OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
+ "or Debian Stretch), you need to set LD_LIBRARY_PATH to the "
+ "path of OpenSSL 1.0. This only affects downloads.")
+
+ if QSslSocket.supportsSsl():
+ return
+
+ if fatal:
+ errbox = msgbox.msgbox(parent=None,
+ title="SSL error",
+ text="Could not initialize SSL support.",
+ icon=QMessageBox.Critical,
+ plain_text=False)
+ errbox.exec_()
+ sys.exit(usertypes.Exit.err_init)
+
+ assert not fatal
+ log.init.warning(text)
+
+ def _check_backend_modules(self) -> None:
+ """Check for the modules needed for QtWebKit/QtWebEngine."""
+ imports = self._try_import_backends()
+
+ if imports.webkit_available and imports.webengine_available:
+ return
+ elif not imports.webkit_available and not imports.webengine_available:
+ text = ("qutebrowser needs QtWebKit or QtWebEngine, but "
+ "neither could be imported!
"
+ "The errors encountered were:
"
+ "- QtWebKit: {webkit_error}"
+ "
- QtWebEngine: {webengine_error}"
+ "
".format(
+ webkit_error=html.escape(imports.webkit_error),
+ webengine_error=html.escape(imports.webengine_error)))
+ errbox = msgbox.msgbox(parent=None,
+ title="No backend library found!",
+ text=text,
+ icon=QMessageBox.Critical,
+ plain_text=False)
+ errbox.exec_()
+ sys.exit(usertypes.Exit.err_init)
+ elif objects.backend == usertypes.Backend.QtWebKit:
+ if imports.webkit_available:
+ return
+ assert imports.webengine_available
+ self._show_dialog(
+ backend=usertypes.Backend.QtWebKit,
+ because="QtWebKit could not be imported",
+ text="The error encountered was:
{}
".format(
+ html.escape(imports.webkit_error))
+ )
+ elif objects.backend == usertypes.Backend.QtWebEngine:
+ if imports.webengine_available:
+ return
+ assert imports.webkit_available
+ self._show_dialog(
+ backend=usertypes.Backend.QtWebEngine,
+ because="QtWebEngine could not be imported",
+ text="The error encountered was:
{}
".format(
+ html.escape(imports.webengine_error))
+ )
+
+ raise utils.Unreachable
+
+ def _handle_cache_nuking(self) -> None:
+ """Nuke the QtWebEngine cache if the Qt version changed.
+
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532
+ """
+ if not configfiles.state.qt_version_changed:
+ return
+
+ # Only nuke the cache in cases where we know there are problems.
+ # It seems these issues started with Qt 5.12.
+ # They should be fixed with Qt 5.12.5:
+ # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408
+ affected = (qtutils.version_check('5.12', compiled=False) and not
+ qtutils.version_check('5.12.5', compiled=False))
+ if not affected:
+ return
+
+ log.init.info("Qt version changed, nuking QtWebEngine cache")
+ cache_dir = os.path.join(standarddir.cache(), 'webengine')
+ if os.path.exists(cache_dir):
+ shutil.rmtree(cache_dir)
+
+ def _assert_backend(self, backend: usertypes.Backend) -> None:
+ assert objects.backend == backend, objects.backend
+
+ def check(self) -> None:
+ """Run all checks."""
+ self._check_backend_modules()
+ if objects.backend == usertypes.Backend.QtWebEngine:
+ self._handle_ssl_support()
+ self._handle_wayland()
+ self._nvidia_shader_workaround()
+ self._handle_nouveau_graphics()
+ self._handle_cache_nuking()
+ else:
+ self._assert_backend(usertypes.Backend.QtWebKit)
+ self._handle_ssl_support(fatal=True)
-def _handle_cache_nuking():
- """Nuke the QtWebEngine cache if the Qt version changed.
-
- WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532
- """
- if not configfiles.state.qt_version_changed:
- return
-
- # Only nuke the cache in cases where we know there are problems.
- # It seems these issues started with Qt 5.12.
- # They should be fixed with Qt 5.12.5:
- # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408
- affected = (qtutils.version_check('5.12', compiled=False) and not
- qtutils.version_check('5.12.5', compiled=False))
- if not affected:
- return
-
- log.init.info("Qt version changed, nuking QtWebEngine cache")
- cache_dir = os.path.join(standarddir.cache(), 'webengine')
- if os.path.exists(cache_dir):
- shutil.rmtree(cache_dir)
-
-
-def init():
- """Check for various issues related to QtWebKit/QtWebEngine."""
- _check_backend_modules()
- if objects.backend == usertypes.Backend.QtWebEngine:
- _handle_ssl_support()
- _handle_wayland()
- _nvidia_shader_workaround()
- _handle_nouveau_graphics()
- _handle_cache_nuking()
- else:
- assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
- _handle_ssl_support(fatal=True)
+def init(*, quitter: 'app.Quitter',
+ args: argparse.Namespace,
+ save_manager: savemanager.SaveManager) -> None:
+ """Run all checks."""
+ checker = _BackendProblemChecker(quitter=quitter,
+ no_err_windows=args.no_err_windows,
+ save_manager=save_manager)
+ checker.check()
diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py
index 138ce76c8..82d6caf98 100644
--- a/qutebrowser/misc/consolewidget.py
+++ b/qutebrowser/misc/consolewidget.py
@@ -21,6 +21,7 @@
import sys
import code
+import typing
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
@@ -31,6 +32,9 @@ from qutebrowser.misc import cmdhistory, miscwidgets
from qutebrowser.utils import utils, objreg
+console_widget = None
+
+
class ConsoleLineEdit(miscwidgets.CommandLineEdit):
"""A QLineEdit which executes entered code and provides a history.
@@ -211,3 +215,9 @@ class ConsoleWidget(QWidget):
def _curprompt(self):
"""Get the prompt which is visible currently."""
return sys.ps2 if self._more else sys.ps1
+
+
+def init():
+ """Initialize a global console."""
+ global console_widget
+ console_widget = ConsoleWidget()
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py
index 286a4c339..53e203866 100644
--- a/qutebrowser/misc/crashsignal.py
+++ b/qutebrowser/misc/crashsignal.py
@@ -162,6 +162,10 @@ class CrashHandler(QObject):
all_objects)
self._crash_dialog.show()
+ @pyqtSlot()
+ def shutdown(self):
+ self.destroy_crashlogfile()
+
def destroy_crashlogfile(self):
"""Clean up the crash log file and delete it."""
if self._crash_log_file is None:
diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py
index f7bf6e826..e16effb62 100644
--- a/qutebrowser/misc/ipc.py
+++ b/qutebrowser/misc/ipc.py
@@ -395,6 +395,7 @@ class IPCServer(QObject):
log.ipc.debug("Touching {}".format(path))
os.utime(path)
+ @pyqtSlot()
def shutdown(self):
"""Shut down the IPC server cleanly."""
log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py
index 241951b84..5b31d64c6 100644
--- a/qutebrowser/misc/msgbox.py
+++ b/qutebrowser/misc/msgbox.py
@@ -24,7 +24,7 @@ import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
-from qutebrowser.utils import objreg
+from qutebrowser.misc import objects
class DummyBox:
@@ -51,8 +51,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
Return:
A new QMessageBox.
"""
- args = objreg.get('args')
- if args.no_err_windows:
+ if objects.args.no_err_windows:
print('Message box: {}; {}'.format(title, text), file=sys.stderr)
return DummyBox()
diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py
index e2885ca22..5415ab453 100644
--- a/qutebrowser/misc/objects.py
+++ b/qutebrowser/misc/objects.py
@@ -23,6 +23,7 @@
# earlyinit.
import typing
+import argparse
if typing.TYPE_CHECKING:
from qutebrowser.utils import usertypes
@@ -40,3 +41,4 @@ class NoBackend:
backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend]
commands = {} # type: typing.Dict[str, command.Command]
debug_flags = set() # type: typing.Set[str]
+args = typing.cast(argparse.Namespace, None)
diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py
index 384522bf0..f2a551c7d 100644
--- a/qutebrowser/misc/savemanager.py
+++ b/qutebrowser/misc/savemanager.py
@@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.api import cmdutils
-from qutebrowser.utils import utils, log, message, usertypes
+from qutebrowser.utils import utils, log, message, usertypes, error
class Saveable:
@@ -201,3 +201,14 @@ class SaveManager(QObject):
except OSError as e:
message.error("Could not save {}: {}".format(key, e))
log.save.debug(":save saved {}".format(', '.join(what)))
+
+ @pyqtSlot()
+ def shutdown(self):
+ """Save all saveables when shutting down."""
+ for key in self.saveables:
+ try:
+ self.save(key, is_exit=True)
+ except OSError as e:
+ error.handle_fatal_exc(
+ e, self._args, "Error while saving!",
+ pre_text="Error while saving {}".format(key))
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index d061ed6e4..62fce370f 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -25,7 +25,7 @@ import itertools
import urllib
import typing
-from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
+from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer, pyqtSlot
from PyQt5.QtWidgets import QApplication
import yaml
@@ -44,6 +44,9 @@ class Sentinel:
default = Sentinel()
+session_manager = typing.cast('SessionManager', None)
+
+ArgType = typing.Union[str, Sentinel]
def init(parent=None):
@@ -58,8 +61,14 @@ def init(parent=None):
except FileExistsError:
pass
+ global session_manager
session_manager = SessionManager(base_path, parent)
- objreg.register('session-manager', session_manager)
+ objreg.register('session-manager', session_manager, command_only=True)
+
+
+@pyqtSlot()
+def shutdown():
+ session_manager.delete_autosave()
class SessionError(Exception):
@@ -518,7 +527,7 @@ class SessionManager(QObject):
@cmdutils.argument('name', completion=miscmodels.session)
@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,
+ def session_save(self, name: ArgType = default,
current: bool = False,
quiet: bool = False,
force: bool = False,
diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py
index 9bc8c620c..35ac54c85 100644
--- a/qutebrowser/misc/throttle.py
+++ b/qutebrowser/misc/throttle.py
@@ -64,8 +64,9 @@ class Throttle(QObject):
self._timer = usertypes.Timer(self, 'throttle-timer')
self._timer.setSingleShot(True)
- def _call_pending(self):
+ def _call_pending(self) -> None:
"""Start a pending call."""
+ assert self._pending_call is not None
self._func(*self._pending_call.args, **self._pending_call.kwargs)
self._pending_call = None
self._last_call_ms = int(time.monotonic() * 1000)
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index 93cf20be8..1811142d7 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -26,7 +26,7 @@ import os
import traceback
from PyQt5.QtCore import QUrl
-from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
+from PyQt5.QtWidgets import QApplication
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
@@ -50,8 +50,7 @@ def later(ms: int, command: str, win_id: int) -> None:
if ms < 0:
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)
+ timer = usertypes.Timer(name='later', parent=QApplication.instance())
try:
timer.setSingleShot(True)
try:
@@ -107,44 +106,43 @@ def run_with_count(count_arg: int, command: str, win_id: int,
@cmdutils.register()
-def clear_messages():
+def clear_messages() -> None:
"""Clear all message notifications."""
message.global_bridge.clear_messages.emit()
@cmdutils.register(debug=True)
-def debug_all_objects():
+def debug_all_objects() -> None:
"""Print a list of all objects to the debug log."""
s = debug.get_all_objects()
log.misc.debug(s)
@cmdutils.register(debug=True)
-def debug_cache_stats():
+def debug_cache_stats() -> None:
"""Print LRU cache stats."""
debugcachestats.debug_cache_stats()
@cmdutils.register(debug=True)
-def debug_console():
+def debug_console() -> None:
"""Show the debugging console."""
- try:
- con_widget = objreg.get('debug-console')
- except KeyError:
+ if consolewidget.console_widget is None:
log.misc.debug('initializing debug console')
- con_widget = consolewidget.ConsoleWidget()
- objreg.register('debug-console', con_widget)
+ consolewidget.init()
- if con_widget.isVisible():
+ assert consolewidget.console_widget is not None
+
+ if consolewidget.console_widget.isVisible():
log.misc.debug('hiding debug console')
- con_widget.hide()
+ consolewidget.console_widget.hide()
else:
log.misc.debug('showing debug console')
- con_widget.show()
+ consolewidget.console_widget.show()
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
-def debug_pyeval(s, file=False, quiet=False):
+def debug_pyeval(s: str, file: bool = False, quiet: bool = False) -> None:
"""Evaluate a python string and display the results as a web page.
Args:
@@ -182,7 +180,7 @@ def debug_pyeval(s, file=False, quiet=False):
@cmdutils.register(debug=True)
-def debug_set_fake_clipboard(s=None):
+def debug_set_fake_clipboard(s: str = None) -> None:
"""Put data into the fake clipboard and enable logging, used for tests.
Args:
@@ -197,7 +195,7 @@ def debug_set_fake_clipboard(s=None):
@cmdutils.register()
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
@cmdutils.argument('count', value=cmdutils.Value.count)
-def repeat_command(win_id, count=None):
+def repeat_command(win_id: int, count: int = None) -> None:
"""Repeat the last executed command.
Args:
@@ -234,8 +232,11 @@ def debug_log_level(level: str) -> None:
Args:
level: The log level to set.
"""
+ if log.console_handler is None:
+ raise cmdutils.CommandError("No log.console_handler. Not attached "
+ "to a console?")
+
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()])
@@ -265,7 +266,7 @@ def debug_log_filter(filters: str) -> None:
@cmdutils.register()
@cmdutils.argument('current_win_id', value=cmdutils.Value.win_id)
-def window_only(current_win_id):
+def window_only(current_win_id: int) -> None:
"""Close all windows except for the current one."""
for win_id, window in objreg.window_registry.items():
@@ -279,7 +280,7 @@ def window_only(current_win_id):
@cmdutils.register()
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
-def version(win_id, paste=False):
+def version(win_id: int, paste: bool = False) -> None:
"""Show version information.
Args:
diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py
index 7bb2d6992..383a19df6 100644
--- a/qutebrowser/utils/debug.py
+++ b/qutebrowser/utils/debug.py
@@ -24,34 +24,36 @@ import inspect
import logging
import functools
import datetime
+import typing
+import types
-from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject
+from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, utils, qtutils, objreg
-def log_events(klass):
+def log_events(klass: typing.Type) -> typing.Type:
"""Class decorator to log Qt events."""
old_event = klass.event
@functools.wraps(old_event)
- def new_event(self, e, *args, **kwargs):
+ def new_event(self: typing.Any, e: QEvent) -> bool:
"""Wrapper for event() which logs events."""
log.misc.debug("Event in {}: {}".format(utils.qualname(klass),
qenum_key(QEvent, e.type())))
- return old_event(self, e, *args, **kwargs)
+ return old_event(self, e)
klass.event = new_event
return klass
-def log_signals(obj):
+def log_signals(obj: QObject) -> QObject:
"""Log all signals of an object or class.
Can be used as class decorator.
"""
- def log_slot(obj, signal, *args):
+ def log_slot(obj: QObject, signal: pyqtSignal, *args: typing.Any) -> None:
"""Slot connected to a signal to log it."""
dbg = dbg_signal(signal, args)
try:
@@ -60,7 +62,7 @@ def log_signals(obj):
r = ''
log.signals.debug("Signal in {}: {}".format(r, dbg))
- def connect_log_slot(obj):
+ def connect_log_slot(obj: QObject) -> None:
"""Helper function to connect all signals to a logging slot."""
metaobj = obj.metaObject()
for i in range(metaobj.methodCount()):
@@ -77,23 +79,27 @@ def log_signals(obj):
pass
if inspect.isclass(obj):
- old_init = obj.__init__
+ old_init = obj.__init__ # type: ignore
@functools.wraps(old_init)
- def new_init(self, *args, **kwargs):
+ def new_init(self: typing.Any,
+ *args: typing.Any,
+ **kwargs: typing.Any) -> None:
"""Wrapper for __init__() which logs signals."""
- ret = old_init(self, *args, **kwargs)
+ old_init(self, *args, **kwargs)
connect_log_slot(self)
- return ret
- obj.__init__ = new_init
+ obj.__init__ = new_init # type: ignore
else:
connect_log_slot(obj)
return obj
-def qenum_key(base, value, add_base=False, klass=None):
+def qenum_key(base: typing.Type,
+ value: int,
+ add_base: bool = False,
+ klass: typing.Type = None) -> str:
"""Convert a Qt Enum value to its key as a string.
Args:
@@ -132,7 +138,10 @@ def qenum_key(base, value, add_base=False, klass=None):
return ret
-def qflags_key(base, value, add_base=False, klass=None):
+def qflags_key(base: typing.Type,
+ value: int,
+ add_base: bool = False,
+ klass: typing.Type = None) -> str:
"""Convert a Qt QFlags value to its keys as string.
Note: Passing a combined value (such as Qt.AlignCenter) will get the names
@@ -176,7 +185,7 @@ def qflags_key(base, value, add_base=False, klass=None):
return '|'.join(names)
-def signal_name(sig):
+def signal_name(sig: pyqtSignal) -> str:
"""Get a cleaned up name of a signal.
Args:
@@ -185,11 +194,13 @@ def signal_name(sig):
Return:
The cleaned up signal name.
"""
- m = re.fullmatch(r'[0-9]+(.*)\(.*\)', sig.signal)
+ m = re.fullmatch(r'[0-9]+(.*)\(.*\)', sig.signal) # type: ignore
+ assert m is not None
return m.group(1)
-def format_args(args=None, kwargs=None):
+def format_args(args: typing.Sequence = None,
+ kwargs: typing.Mapping = None) -> str:
"""Format a list of arguments/kwargs to a function-call like string."""
if args is not None:
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
@@ -201,7 +212,7 @@ def format_args(args=None, kwargs=None):
return ', '.join(arglist)
-def dbg_signal(sig, args):
+def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
"""Get a string representation of a signal for debugging.
Args:
@@ -214,7 +225,10 @@ def dbg_signal(sig, args):
return '{}({})'.format(signal_name(sig), format_args(args))
-def format_call(func, args=None, kwargs=None, full=True):
+def format_call(func: typing.Callable,
+ args: typing.Sequence = None,
+ kwargs: typing.Mapping = None,
+ full: bool = True) -> str:
"""Get a string representation of a function calls with the given args.
Args:
@@ -240,7 +254,8 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
Usable as context manager or as decorator.
"""
- def __init__(self, logger, action='operation'):
+ def __init__(self, logger: typing.Union[logging.Logger, str],
+ action: str = 'operation') -> None:
"""Constructor.
Args:
@@ -251,44 +266,49 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
self._logger = logging.getLogger(logger)
else:
self._logger = logger
- self._started = None
+ self._started = None # type: typing.Optional[datetime.datetime]
self._action = action
- def __enter__(self):
+ def __enter__(self) -> None:
self._started = datetime.datetime.now()
- def __exit__(self, _exc_type, _exc_val, _exc_tb):
+ def __exit__(self, _exc_type: typing.Optional[typing.Type[BaseException]],
+ _exc_val: typing.Optional[BaseException],
+ _exc_tb: typing.Optional[types.TracebackType]) -> bool:
assert self._started is not None
finished = datetime.datetime.now()
delta = (finished - self._started).total_seconds()
self._logger.debug("{} took {} seconds.".format(
self._action.capitalize(), delta))
+ return False
- def __call__(self, func):
+ def __call__(self, func: typing.Callable) -> typing.Callable:
@functools.wraps(func)
- def wrapped(*args, **kwargs):
+ def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
"""Call the original function."""
with self:
- func(*args, **kwargs)
+ return func(*args, **kwargs)
return wrapped
-def _get_widgets():
+def _get_widgets() -> typing.Sequence[str]:
"""Get a string list of all widgets."""
widgets = QApplication.instance().allWidgets()
widgets.sort(key=repr)
return [repr(w) for w in widgets]
-def _get_pyqt_objects(lines, obj, depth=0):
+def _get_pyqt_objects(lines: typing.MutableSequence[str],
+ obj: QObject,
+ depth: int = 0) -> None:
"""Recursive method for get_all_objects to get Qt objects."""
for kid in obj.findChildren(QObject, '', Qt.FindDirectChildrenOnly):
lines.append(' ' * depth + repr(kid))
_get_pyqt_objects(lines, kid, depth + 1)
-def get_all_objects(start_obj=None):
+def get_all_objects(start_obj: QObject = None) -> str:
"""Get all children of an object recursively as a string."""
output = ['']
widget_lines = _get_widgets()
@@ -300,7 +320,7 @@ def get_all_objects(start_obj=None):
if start_obj is None:
start_obj = QApplication.instance()
- pyqt_lines = []
+ pyqt_lines = [] # type: typing.List[str]
_get_pyqt_objects(pyqt_lines, start_obj)
pyqt_lines = [' ' + e for e in pyqt_lines]
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines)))
diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py
index cf346fb81..94c348292 100644
--- a/qutebrowser/utils/docutils.py
+++ b/qutebrowser/utils/docutils.py
@@ -25,18 +25,19 @@ import inspect
import os.path
import collections
import enum
+import typing
import qutebrowser
from qutebrowser.utils import log, utils
-def is_git_repo():
+def is_git_repo() -> bool:
"""Check if we're running from a git repository."""
gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git')
return os.path.isdir(gitfolder)
-def docs_up_to_date(path):
+def docs_up_to_date(path: str) -> bool:
"""Check if the generated html documentation is up to date.
Args:
@@ -79,17 +80,18 @@ class DocstringParser:
State = enum.Enum('State', ['short', 'desc', 'desc_hidden',
'arg_start', 'arg_inside', 'misc'])
- def __init__(self, func):
+ def __init__(self, func: typing.Callable) -> None:
"""Constructor.
Args:
func: The function to parse the docstring for.
"""
self._state = self.State.short
- self._cur_arg_name = None
- self._short_desc_parts = []
- self._long_desc_parts = []
- self.arg_descs = collections.OrderedDict()
+ self._cur_arg_name = None # type: typing.Optional[str]
+ self._short_desc_parts = [] # type: typing.List[str]
+ self._long_desc_parts = [] # type: typing.List[str]
+ self.arg_descs = collections.OrderedDict(
+ ) # type: typing.Dict[str, typing.Union[str, typing.List[str]]]
doc = inspect.getdoc(func)
handlers = {
self.State.short: self._parse_short,
@@ -104,7 +106,8 @@ class DocstringParser:
log.commands.warning(
"Function {}() from {} has no docstring".format(
utils.qualname(func),
- inspect.getsourcefile(func)))
+ # https://github.com/python/typeshed/pull/3295
+ inspect.getsourcefile(func))) # type: ignore
self.long_desc = ""
self.short_desc = ""
return
@@ -121,25 +124,25 @@ class DocstringParser:
self.long_desc = ' '.join(self._long_desc_parts)
self.short_desc = ' '.join(self._short_desc_parts)
- def _process_arg(self, line):
+ def _process_arg(self, line: str) -> None:
"""Helper method to process a line like 'fooarg: Blah blub'."""
self._cur_arg_name, argdesc = line.split(':', maxsplit=1)
self._cur_arg_name = self._cur_arg_name.strip().lstrip('*')
self.arg_descs[self._cur_arg_name] = [argdesc.strip()]
- def _skip(self, line):
+ def _skip(self, line: str) -> None:
"""Handler to ignore everything until we get 'Args:'."""
if line.startswith('Args:'):
self._state = self.State.arg_start
- def _parse_short(self, line):
+ def _parse_short(self, line: str) -> None:
"""Parse the short description (first block) in the docstring."""
if not line:
self._state = self.State.desc
else:
self._short_desc_parts.append(line.strip())
- def _parse_desc(self, line):
+ def _parse_desc(self, line: str) -> None:
"""Parse the long description in the docstring."""
if line.startswith('Args:'):
self._state = self.State.arg_start
@@ -148,22 +151,27 @@ class DocstringParser:
elif line.strip():
self._long_desc_parts.append(line.strip())
- def _parse_arg_start(self, line):
+ def _parse_arg_start(self, line: str) -> None:
"""Parse first argument line."""
self._process_arg(line)
self._state = self.State.arg_inside
- def _parse_arg_inside(self, line):
+ def _parse_arg_inside(self, line: str) -> bool:
"""Parse subsequent argument lines."""
argname = self._cur_arg_name
+ assert argname is not None
+
+ descs = self.arg_descs[argname]
+ assert isinstance(descs, list)
+
if re.fullmatch(r'[A-Z][a-z]+:', line):
- if not self.arg_descs[argname][-1].strip():
- self.arg_descs[argname] = self.arg_descs[argname][:-1]
+ if not descs[-1].strip():
+ del descs[-1]
return True
elif not line.strip():
- self.arg_descs[argname].append('\n\n')
+ descs.append('\n\n')
elif line[4:].startswith(' '):
- self.arg_descs[argname].append(line.strip() + '\n')
+ descs.append(line.strip() + '\n')
else:
self._process_arg(line)
return False
diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py
index fe8077526..c5c1974be 100644
--- a/qutebrowser/utils/error.py
+++ b/qutebrowser/utils/error.py
@@ -19,12 +19,14 @@
"""Tools related to error printing/displaying."""
+import argparse
+
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import log, utils
-def _get_name(exc):
+def _get_name(exc: BaseException) -> str:
"""Get a suitable exception name as a string."""
prefixes = ['qutebrowser', 'builtins']
name = utils.qualname(exc.__class__)
@@ -35,7 +37,11 @@ def _get_name(exc):
return name
-def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
+def handle_fatal_exc(exc: BaseException,
+ args: argparse.Namespace,
+ title: str, *,
+ pre_text: str = '',
+ post_text: str = '') -> None:
"""Handle a fatal "expected" exception by displaying an error box.
If --no-err-windows is given as argument, the text is logged to the error
diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py
index 42da9759d..f456d93c9 100644
--- a/qutebrowser/utils/javascript.py
+++ b/qutebrowser/utils/javascript.py
@@ -19,11 +19,15 @@
"""Utilities related to javascript interaction."""
+import typing
from qutebrowser.utils import jinja
+_InnerJsArgType = typing.Union[None, str, bool, int, float]
+_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]]
-def string_escape(text):
+
+def string_escape(text: str) -> str:
"""Escape values special to javascript in strings.
With this we should be able to use something like:
@@ -49,7 +53,7 @@ def string_escape(text):
return text
-def to_js(arg):
+def to_js(arg: _JsArgType) -> str:
"""Convert the given argument so it's the equivalent in JS."""
if arg is None:
return 'undefined'
@@ -66,7 +70,7 @@ def to_js(arg):
arg, type(arg).__name__))
-def assemble(module, function, *args):
+def assemble(module: str, function: str, *args: _JsArgType) -> str:
"""Assemble a javascript file and a function call."""
js_args = ', '.join(to_js(arg) for arg in args)
if module == 'window':
@@ -77,7 +81,7 @@ def assemble(module, function, *args):
return code
-def wrap_global(name, *sources):
+def wrap_global(name: str, *sources: str) -> str:
"""Wrap a script using window._qutebrowser."""
template = jinja.js_environment.get_template('global_wrapper.js')
return template.render(code='\n'.join(sources), name=name)
diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py
index bbe3c23f6..76987ddaa 100644
--- a/qutebrowser/utils/jinja.py
+++ b/qutebrowser/utils/jinja.py
@@ -61,10 +61,14 @@ class Loader(jinja2.BaseLoader):
_subdir: The subdirectory to find templates in.
"""
- def __init__(self, subdir):
+ def __init__(self, subdir: str) -> None:
self._subdir = subdir
- def get_source(self, _env, template):
+ def get_source(
+ self,
+ _env: jinja2.Environment,
+ template: str
+ ) -> typing.Tuple[str, str, typing.Callable[[], bool]]:
path = os.path.join(self._subdir, template)
try:
source = utils.read_file(path)
@@ -82,7 +86,7 @@ class Environment(jinja2.Environment):
"""Our own jinja environment which is more strict."""
- def __init__(self):
+ def __init__(self) -> None:
super().__init__(loader=Loader('html'),
autoescape=lambda _name: self._autoescape,
undefined=jinja2.StrictUndefined)
@@ -94,29 +98,31 @@ class Environment(jinja2.Environment):
self._autoescape = True
@contextlib.contextmanager
- def no_autoescape(self):
+ def no_autoescape(self) -> typing.Iterator[None]:
"""Context manager to temporarily turn off autoescaping."""
self._autoescape = False
yield
self._autoescape = True
- def _resource_url(self, path):
+ def _resource_url(self, path: str) -> str:
"""Load images from a relative path (to qutebrowser).
Arguments:
path: The relative path to the image
"""
image = utils.resource_filename(path)
- return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
+ url = QUrl.fromLocalFile(image)
+ urlstr = url.toString(QUrl.FullyEncoded) # type: ignore
+ return urlstr
- def _data_url(self, path):
+ def _data_url(self, path: str) -> str:
"""Get a data: url for the broken qutebrowser logo."""
data = utils.read_file(path, binary=True)
filename = utils.resource_filename(path)
mimetype = utils.guess_mimetype(filename)
return urlutils.data_url(mimetype, data).toString()
- def getattr(self, obj, attribute):
+ def getattr(self, obj: typing.Any, attribute: str) -> typing.Any:
"""Override jinja's getattr() to be less clever.
This means it doesn't fall back to __getitem__, and it doesn't hide
@@ -125,7 +131,7 @@ class Environment(jinja2.Environment):
return getattr(obj, attribute)
-def render(template, **kwargs):
+def render(template: str, **kwargs: typing.Any) -> str:
"""Render the given template and pass the given arguments to it."""
return environment.get_template(template).render(**kwargs)
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 168d5f52d..148495bfb 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -31,6 +31,8 @@ import traceback
import warnings
import json
import inspect
+import typing
+import argparse
from PyQt5 import QtCore
# Optional imports
@@ -91,7 +93,10 @@ LOG_LEVELS = {
}
-def vdebug(self, msg, *args, **kwargs):
+def vdebug(self: logging.Logger,
+ msg: str,
+ *args: typing.Any,
+ **kwargs: typing.Any) -> None:
"""Log with a VDEBUG level.
VDEBUG is used when a debug message is rather verbose, and probably of
@@ -100,7 +105,7 @@ def vdebug(self, msg, *args, **kwargs):
"""
if self.isEnabledFor(VDEBUG_LEVEL):
# pylint: disable=protected-access
- self._log(VDEBUG_LEVEL, msg, args, **kwargs)
+ self._log(VDEBUG_LEVEL, msg, args, **kwargs) # type: ignore
# pylint: enable=protected-access
@@ -151,12 +156,12 @@ LOGGER_NAMES = [
]
-ram_handler = None
-console_handler = None
+ram_handler = None # type: typing.Optional[RAMHandler]
+console_handler = None # type: typing.Optional[logging.Handler]
console_filter = None
-def stub(suffix=''):
+def stub(suffix: str = '') -> None:
"""Show a STUB: message for the calling function."""
try:
function = inspect.stack()[1][3]
@@ -169,7 +174,7 @@ def stub(suffix=''):
misc.warning(text)
-def init_log(args):
+def init_log(args: argparse.Namespace) -> None:
"""Init loggers based on the argparse namespace passed."""
level = args.loglevel.upper()
try:
@@ -208,13 +213,18 @@ def init_log(args):
root.setLevel(logging.NOTSET)
logging.captureWarnings(True)
_init_py_warnings()
- QtCore.qInstallMessageHandler(qt_message_handler)
+ QtCore.qInstallMessageHandler(qt_message_handler) # type: ignore
global _log_inited, _args
_log_inited = True
_args = args
-def _init_py_warnings():
+@QtCore.pyqtSlot()
+def shutdown_log() -> None:
+ QtCore.qInstallMessageHandler(None)
+
+
+def _init_py_warnings() -> None:
"""Initialize Python warning handling."""
warnings.simplefilter('default')
warnings.filterwarnings('ignore', module='pdb', category=ResourceWarning)
@@ -226,7 +236,7 @@ def _init_py_warnings():
@contextlib.contextmanager
-def disable_qt_msghandler():
+def disable_qt_msghandler() -> typing.Iterator[None]:
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = QtCore.qInstallMessageHandler(None)
try:
@@ -236,7 +246,7 @@ def disable_qt_msghandler():
@contextlib.contextmanager
-def ignore_py_warnings(**kwargs):
+def ignore_py_warnings(**kwargs: typing.Any) -> typing.Iterator[None]:
"""Contextmanager to temporarily disable certain Python warnings."""
warnings.filterwarnings('ignore', **kwargs)
yield
@@ -244,7 +254,13 @@ def ignore_py_warnings(**kwargs):
_init_py_warnings()
-def _init_handlers(level, color, force_color, json_logging, ram_capacity):
+def _init_handlers(
+ level: int,
+ color: bool,
+ force_color: bool,
+ json_logging: bool,
+ ram_capacity: int
+) -> typing.Tuple[logging.StreamHandler, typing.Optional['RAMHandler']]:
"""Init log handlers.
Args:
@@ -281,7 +297,7 @@ def _init_handlers(level, color, force_color, json_logging, ram_capacity):
return console_handler, ram_handler
-def get_console_format(level):
+def get_console_format(level: int) -> str:
"""Get the log format the console logger should use.
Args:
@@ -293,7 +309,13 @@ def get_console_format(level):
return EXTENDED_FMT if level <= logging.DEBUG else SIMPLE_FMT
-def _init_formatters(level, color, force_color, json_logging):
+def _init_formatters(
+ level: int,
+ color: bool,
+ force_color: bool,
+ json_logging: bool
+) -> typing.Tuple[typing.Union['JSONFormatter', 'ColoredFormatter'],
+ 'ColoredFormatter', 'HTMLFormatter', bool]:
"""Init log formatters.
Args:
@@ -316,8 +338,8 @@ def _init_formatters(level, color, force_color, json_logging):
return None, ram_formatter, html_formatter, False # type: ignore
if json_logging:
- console_formatter = JSONFormatter()
- return console_formatter, ram_formatter, html_formatter, False
+ json_formatter = JSONFormatter()
+ return json_formatter, ram_formatter, html_formatter, False
use_colorama = False
color_supported = os.name == 'posix' or colorama
@@ -334,24 +356,24 @@ def _init_formatters(level, color, force_color, json_logging):
return console_formatter, ram_formatter, html_formatter, use_colorama
-def change_console_formatter(level):
+def change_console_formatter(level: int) -> None:
"""Change console formatter based on level.
Args:
level: The numeric logging level
"""
- if not isinstance(console_handler.formatter, ColoredFormatter):
- # JSON Formatter being used for end2end tests
- pass
+ assert console_handler is not None
- use_colors = console_handler.formatter.use_colors
+ old_formatter = typing.cast(ColoredFormatter, console_handler.formatter)
console_fmt = get_console_format(level)
console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{',
- use_colors=use_colors)
+ use_colors=old_formatter.use_colors)
console_handler.setFormatter(console_formatter)
-def qt_message_handler(msg_type, context, msg):
+def qt_message_handler(msg_type: QtCore.QtMsgType,
+ context: QtCore.QMessageLogContext,
+ msg: str) -> None:
"""Qt message handler to redirect qWarning etc. to the logging system.
Args:
@@ -456,13 +478,14 @@ def qt_message_handler(msg_type, context, msg):
level = qt_to_logging[msg_type]
if context.function is None:
- func = 'none'
+ func = 'none' # type: ignore
elif ':' in context.function:
func = '"{}"'.format(context.function)
else:
func = context.function
- if context.category is None or context.category == 'default':
+ if (context.category is None or # type: ignore
+ context.category == 'default'):
name = 'qt'
else:
name = 'qt-' + context.category
@@ -474,17 +497,19 @@ def qt_message_handler(msg_type, context, msg):
" pacman -S libxkbcommon-x11")
faulthandler.disable()
+ assert _args is not None
if _args.debug:
- stack = ''.join(traceback.format_stack())
+ stack = ''.join(traceback.format_stack()) # type: typing.Optional[str]
else:
stack = None
+
record = qt.makeRecord(name, level, context.file, context.line, msg, (),
None, func, sinfo=stack)
qt.handle(record)
@contextlib.contextmanager
-def hide_qt_warning(pattern, logger='qt'):
+def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]:
"""Hide Qt warnings matching the given regex."""
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
@@ -503,11 +528,11 @@ class QtWarningFilter(logging.Filter):
_pattern: The start of the message.
"""
- def __init__(self, pattern):
+ def __init__(self, pattern: str) -> None:
super().__init__()
self._pattern = pattern
- def filter(self, record):
+ def filter(self, record: logging.LogRecord) -> bool:
"""Determine if the specified record is to be logged."""
do_log = not record.msg.strip().startswith(self._pattern)
return do_log
@@ -525,12 +550,13 @@ class LogFilter(logging.Filter):
negated: Whether names is a list of records to log or to suppress.
"""
- def __init__(self, names, negate=False):
+ def __init__(self, names: typing.Optional[typing.Iterable[str]],
+ negate: bool = False) -> None:
super().__init__()
self.names = names
self.negated = negate
- def filter(self, record):
+ def filter(self, record: logging.LogRecord) -> bool:
"""Determine if the specified record is to be logged."""
if self.names is None:
return True
@@ -558,20 +584,22 @@ class RAMHandler(logging.Handler):
_data: A deque containing the logging records.
"""
- def __init__(self, capacity):
+ def __init__(self, capacity: int) -> None:
super().__init__()
- self.html_formatter = None
+ self.html_formatter = None # type: typing.Optional[HTMLFormatter]
if capacity != -1:
- self._data = collections.deque(maxlen=capacity)
+ self._data = collections.deque(
+ maxlen=capacity
+ ) # type: typing.MutableSequence[logging.LogRecord]
else:
self._data = collections.deque()
- def emit(self, record):
+ def emit(self, record: logging.LogRecord) -> None:
if record.levelno >= logging.DEBUG:
# We don't log VDEBUG to RAM.
self._data.append(record)
- def dump_log(self, html=False, level='vdebug'):
+ def dump_log(self, html: bool = False, level: str = 'vdebug') -> str:
"""Dump the complete formatted log data as string.
FIXME: We should do all the HTML formatter via jinja2.
@@ -579,7 +607,13 @@ class RAMHandler(logging.Handler):
https://github.com/qutebrowser/qutebrowser/issues/34
"""
minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL)
- fmt = self.html_formatter.format if html else self.format
+
+ if html:
+ assert self.html_formatter is not None
+ fmt = self.html_formatter.format
+ else:
+ fmt = self.format
+
self.acquire()
try:
lines = [fmt(record)
@@ -589,7 +623,7 @@ class RAMHandler(logging.Handler):
self.release()
return '\n'.join(lines)
- def change_log_capacity(self, capacity):
+ def change_log_capacity(self, capacity: int) -> None:
self._data = collections.deque(self._data, maxlen=capacity)
@@ -601,11 +635,14 @@ class ColoredFormatter(logging.Formatter):
use_colors: Whether to do colored logging or not.
"""
- def __init__(self, fmt, datefmt, style, *, use_colors):
+ def __init__(self, fmt: str,
+ datefmt: str,
+ style: str, *,
+ use_colors: bool) -> None:
super().__init__(fmt, datefmt, style)
self.use_colors = use_colors
- def format(self, record):
+ def format(self, record: logging.LogRecord) -> str:
if self.use_colors:
color_dict = dict(COLOR_ESCAPES)
color_dict['reset'] = RESET_ESCAPE
@@ -628,7 +665,9 @@ class HTMLFormatter(logging.Formatter):
_colordict: The colordict passed to the logger.
"""
- def __init__(self, fmt, datefmt, log_colors):
+ def __init__(self, fmt: str,
+ datefmt: str,
+ log_colors: typing.Mapping[str, str]) -> None:
"""Constructor.
Args:
@@ -637,22 +676,22 @@ class HTMLFormatter(logging.Formatter):
log_colors: The colors to use for logging levels.
"""
super().__init__(fmt, datefmt)
- self._log_colors = log_colors
- self._colordict = {}
+ self._log_colors = log_colors # type: typing.Mapping[str, str]
+ self._colordict = {} # type: typing.Mapping[str, str]
# We could solve this nicer by using CSS, but for this simple case this
# works.
for color in COLORS:
self._colordict[color] = ''.format(color)
self._colordict['reset'] = ''
- def format(self, record):
+ def format(self, record: logging.LogRecord) -> str:
record_clone = copy.copy(record)
record_clone.__dict__.update(self._colordict)
if record_clone.levelname in self._log_colors:
color = self._log_colors[record_clone.levelname]
- record_clone.log_color = self._colordict[color]
+ record_clone.log_color = self._colordict[color] # type: ignore
else:
- record_clone.log_color = ''
+ record_clone.log_color = '' # type: ignore
for field in ['msg', 'filename', 'funcName', 'levelname', 'module',
'name', 'pathname', 'processName', 'threadName']:
data = str(getattr(record_clone, field))
@@ -662,8 +701,10 @@ class HTMLFormatter(logging.Formatter):
msg += self._colordict['reset']
return msg
- def formatTime(self, record, datefmt=None):
- out = super().formatTime(record, datefmt)
+ def formatTime(self, record: logging.LogRecord,
+ datefmt: str = None) -> str:
+ # https://github.com/python/typeshed/pull/3343
+ out = super().formatTime(record, datefmt) # type: ignore
return pyhtml.escape(out)
@@ -671,7 +712,7 @@ class JSONFormatter(logging.Formatter):
"""Formatter for JSON-encoded log messages."""
- def format(self, record):
+ def format(self, record: logging.LogRecord) -> str:
obj = {}
for field in ['created', 'msecs', 'levelname', 'name', 'module',
'funcName', 'lineno', 'levelno']:
diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py
index 155547abd..2bd30c136 100644
--- a/qutebrowser/utils/message.py
+++ b/qutebrowser/utils/message.py
@@ -23,8 +23,9 @@
"""Message singleton so we don't have to define unneeded signals."""
import traceback
+import typing
-from PyQt5.QtCore import pyqtSignal, QObject
+from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import usertypes, log, utils
@@ -82,11 +83,14 @@ def info(message: str, *, replace: bool = False) -> None:
global_bridge.show(usertypes.MessageLevel.info, message, replace)
-def _build_question(title, text=None, *, mode, default=None, abort_on=(),
- url=None, option=None):
+def _build_question(title: str,
+ text: str = None, *,
+ mode: usertypes.PromptMode,
+ default: typing.Union[None, bool, str] = None,
+ abort_on: typing.Iterable[pyqtSignal] = (),
+ url: QUrl = None,
+ option: bool = None) -> usertypes.Question:
"""Common function for ask/ask_async."""
- if not isinstance(mode, usertypes.PromptMode):
- raise TypeError("Mode {} is no PromptMode member!".format(mode))
question = usertypes.Question()
question.title = title
question.text = text
@@ -106,7 +110,7 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=(),
return question
-def ask(*args, **kwargs):
+def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
"""Ask a modular question in the statusbar (blocking).
Args:
@@ -128,7 +132,10 @@ def ask(*args, **kwargs):
return answer
-def ask_async(title, mode, handler, **kwargs):
+def ask_async(title: str,
+ mode: usertypes.PromptMode,
+ handler: typing.Callable[[typing.Any], None],
+ **kwargs: typing.Any) -> None:
"""Ask an async question in the statusbar.
Args:
@@ -144,8 +151,10 @@ def ask_async(title, mode, handler, **kwargs):
global_bridge.ask(question, blocking=False)
-def confirm_async(*, yes_action, no_action=None, cancel_action=None,
- **kwargs):
+def confirm_async(*, yes_action: typing.Callable[[], None],
+ no_action: typing.Callable[[], None] = None,
+ cancel_action: typing.Callable[[], None] = None,
+ **kwargs: typing.Any) -> usertypes.Question:
"""Ask a yes/no question to the user and execute the given actions.
Args:
@@ -204,12 +213,15 @@ class GlobalMessageBridge(QObject):
mode_left = pyqtSignal(usertypes.KeyMode)
clear_messages = pyqtSignal()
- def __init__(self, parent=None):
+ def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
self._connected = False
- self._cache = []
+ self._cache = [
+ ] # type: typing.List[typing.Tuple[usertypes.MessageLevel, str, bool]]
- def ask(self, question, blocking, *, log_stack=False):
+ def ask(self, question: usertypes.Question,
+ blocking: bool, *,
+ log_stack: bool = False) -> None:
"""Ask a question to the user.
Note this method doesn't return the answer, it only blocks. The caller
@@ -223,14 +235,16 @@ class GlobalMessageBridge(QObject):
"""
self.ask_question.emit(question, blocking)
- def show(self, level, text, replace=False):
+ def show(self, level: usertypes.MessageLevel,
+ text: str,
+ replace: bool = False) -> None:
"""Show the given message."""
if self._connected:
self.show_message.emit(level, text, replace)
else:
self._cache.append((level, text, replace))
- def flush(self):
+ def flush(self) -> None:
"""Flush messages which accumulated while no handler was connected.
This is so we don't miss messages shown during some early init phase.
@@ -256,10 +270,10 @@ class MessageBridge(QObject):
s_set_text = pyqtSignal(str)
s_maybe_reset_text = pyqtSignal(str)
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self)
- def set_text(self, text, *, log_stack=False):
+ def set_text(self, text: str, *, log_stack: bool = False) -> None:
"""Set the normal text of the statusbar.
Args:
@@ -270,7 +284,7 @@ class MessageBridge(QObject):
log.message.debug(text)
self.s_set_text.emit(text)
- def maybe_reset_text(self, text, *, log_stack=False):
+ def maybe_reset_text(self, text: str, *, log_stack: bool = False) -> None:
"""Reset the text in the statusbar if it matches an expected text.
Args:
diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py
index a00460a34..9661c8d2b 100644
--- a/qutebrowser/utils/objreg.py
+++ b/qutebrowser/utils/objreg.py
@@ -22,20 +22,18 @@
import collections
import functools
+import typing
from PyQt5.QtCore import QObject, QTimer
+from PyQt5.QtWidgets import QApplication
+from PyQt5.QtWidgets import QWidget # pylint: disable=unused-import
-from qutebrowser.utils import log
+from qutebrowser.utils import log, usertypes
+if typing.TYPE_CHECKING:
+ from qutebrowser.mainwindow import mainwindow
-class UnsetObject:
-
- """Class for an unset object.
-
- Only used (rather than object) so we can tell pylint to shut up about it.
- """
-
- __slots__ = ()
+_WindowTab = typing.Union[str, int, None]
class RegistryUnavailableError(Exception):
@@ -48,7 +46,9 @@ class NoWindow(Exception):
"""Exception raised by last_window if no window is available."""
-_UNSET = UnsetObject()
+class CommandOnlyError(Exception):
+
+ """Raised when an object is requested which is used for commands only."""
class ObjectRegistry(collections.UserDict):
@@ -59,13 +59,16 @@ class ObjectRegistry(collections.UserDict):
Attributes:
_partial_objs: A dictionary of the connected partial objects.
+ command_only: Objects which are only registered for commands.
"""
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
- self._partial_objs = {}
+ self._partial_objs = {
+ } # type: typing.MutableMapping[str, typing.Callable[[], None]]
+ self.command_only = [] # type: typing.MutableSequence[str]
- def __setitem__(self, name, obj):
+ def __setitem__(self, name: str, obj: typing.Any) -> None:
"""Register an object in the object registry.
Sets a slot to remove QObjects when they are destroyed.
@@ -80,17 +83,17 @@ class ObjectRegistry(collections.UserDict):
if isinstance(obj, QObject):
func = functools.partial(self.on_destroyed, name)
- obj.destroyed.connect(func)
+ obj.destroyed.connect(func) # type: ignore
self._partial_objs[name] = func
super().__setitem__(name, obj)
- def __delitem__(self, name):
+ def __delitem__(self, name: str) -> None:
"""Extend __delitem__ to disconnect the destroyed signal."""
self._disconnect_destroyed(name)
super().__delitem__(name)
- def _disconnect_destroyed(self, name):
+ def _disconnect_destroyed(self, name: str) -> None:
"""Disconnect the destroyed slot if it was connected."""
try:
partial_objs = self._partial_objs
@@ -109,7 +112,7 @@ class ObjectRegistry(collections.UserDict):
pass
del partial_objs[name]
- def on_destroyed(self, name):
+ def on_destroyed(self, name: str) -> None:
"""Schedule removing of a destroyed QObject.
We don't remove the destroyed object immediately because it might still
@@ -119,7 +122,7 @@ class ObjectRegistry(collections.UserDict):
log.destroy.debug("schedule removal: {}".format(name))
QTimer.singleShot(0, functools.partial(self._on_destroyed, name))
- def _on_destroyed(self, name):
+ def _on_destroyed(self, name: str) -> None:
"""Remove a destroyed QObject."""
log.destroy.debug("removed: {}".format(name))
if not hasattr(self, 'data'):
@@ -133,7 +136,7 @@ class ObjectRegistry(collections.UserDict):
except KeyError:
pass
- def dump_objects(self):
+ def dump_objects(self) -> typing.Sequence[str]:
"""Dump all objects as a list of strings."""
lines = []
for name, obj in self.data.items():
@@ -142,7 +145,9 @@ class ObjectRegistry(collections.UserDict):
except RuntimeError:
# Underlying object deleted probably
obj_repr = ''
- lines.append("{}: {}".format(name, obj_repr))
+ suffix = (" (for commands only)" if name in self.command_only
+ else "")
+ lines.append("{}: {}{}".format(name, obj_repr, suffix))
return lines
@@ -152,13 +157,13 @@ global_registry = ObjectRegistry()
window_registry = ObjectRegistry()
-def _get_tab_registry(win_id, tab_id):
+def _get_tab_registry(win_id: _WindowTab,
+ tab_id: _WindowTab) -> ObjectRegistry:
"""Get the registry of a tab."""
if tab_id is None:
raise ValueError("Got tab_id None (win_id {})".format(win_id))
if tab_id == 'current' and win_id is None:
- app = get('app')
- window = app.activeWindow()
+ window = QApplication.activeWindow() # type: typing.Optional[QWidget]
if window is None or not hasattr(window, 'win_id'):
raise RegistryUnavailableError('tab')
win_id = window.win_id
@@ -180,27 +185,32 @@ def _get_tab_registry(win_id, tab_id):
raise RegistryUnavailableError('tab')
-def _get_window_registry(window):
+def _get_window_registry(window: _WindowTab) -> ObjectRegistry:
"""Get the registry of a window."""
if window is None:
raise TypeError("window is None with scope window!")
try:
if window == 'current':
- app = get('app')
- win = app.activeWindow()
+ win = QApplication.activeWindow() # type: typing.Optional[QWidget]
elif window == 'last-focused':
win = last_focused_window()
else:
win = window_registry[window]
except (KeyError, NoWindow):
win = None
+
+ if win is None:
+ raise RegistryUnavailableError('window')
+
try:
return win.registry
except AttributeError:
raise RegistryUnavailableError('window')
-def _get_registry(scope, window=None, tab=None):
+def _get_registry(scope: str,
+ window: _WindowTab = None,
+ tab: _WindowTab = None) -> ObjectRegistry:
"""Get the correct registry for a given scope."""
if window is not None and scope not in ['window', 'tab']:
raise TypeError("window is set with scope {}".format(scope))
@@ -216,24 +226,39 @@ def _get_registry(scope, window=None, tab=None):
raise ValueError("Invalid scope '{}'!".format(scope))
-def get(name, default=_UNSET, scope='global', window=None, tab=None):
+def get(name: str,
+ default: typing.Any = usertypes.UNSET,
+ scope: str = 'global',
+ window: _WindowTab = None,
+ tab: _WindowTab = None,
+ from_command: bool = False) -> typing.Any:
"""Helper function to get an object.
Args:
default: A default to return if the object does not exist.
"""
reg = _get_registry(scope, window, tab)
+ if name in reg.command_only and not from_command:
+ raise CommandOnlyError("{} is only registered for commands"
+ .format(name))
+
try:
return reg[name]
except KeyError:
- if default is not _UNSET:
+ if default is not usertypes.UNSET:
return default
else:
raise
-def register(name, obj, update=False, scope=None, registry=None, window=None,
- tab=None):
+def register(name: str,
+ obj: typing.Any,
+ update: bool = False,
+ scope: str = None,
+ registry: ObjectRegistry = None,
+ window: _WindowTab = None,
+ tab: _WindowTab = None,
+ command_only: bool = False) -> None:
"""Helper function to register an object.
Args:
@@ -244,25 +269,33 @@ def register(name, obj, update=False, scope=None, registry=None, window=None,
if scope is not None and registry is not None:
raise ValueError("scope ({}) and registry ({}) can't be given at the "
"same time!".format(scope, registry))
+
if registry is not None:
reg = registry
else:
if scope is None:
scope = 'global'
reg = _get_registry(scope, window, tab)
+
if not update and name in reg:
raise KeyError("Object '{}' is already registered ({})!".format(
name, repr(reg[name])))
reg[name] = obj
+ if command_only:
+ reg.command_only.append(name)
-def delete(name, scope='global', window=None, tab=None):
+
+def delete(name: str,
+ scope: str = 'global',
+ window: _WindowTab = None,
+ tab: _WindowTab = None) -> None:
"""Helper function to unregister an object."""
reg = _get_registry(scope, window, tab)
del reg[name]
-def dump_objects():
+def dump_objects() -> typing.Sequence[str]:
"""Get all registered objects in all registries as a string."""
blocks = []
lines = []
@@ -275,16 +308,16 @@ def dump_objects():
dump = tab.registry.dump_objects()
data = [' ' + line for line in dump]
blocks.append((' tab-{}'.format(tab_id), data))
- for name, data in blocks:
+ for name, block_data in blocks:
lines.append("")
lines.append("{} object registry - {} objects:".format(
- name, len(data)))
- for line in data:
+ name, len(block_data)))
+ for line in block_data:
lines.append(" {}".format(line))
return lines
-def last_visible_window():
+def last_visible_window() -> 'mainwindow.MainWindow':
"""Get the last visible window, or the last focused window if none."""
try:
return get('last-visible-main-window')
@@ -292,7 +325,7 @@ def last_visible_window():
return last_focused_window()
-def last_focused_window():
+def last_focused_window() -> 'mainwindow.MainWindow':
"""Get the last focused window, or the last window if none."""
try:
return get('last-focused-main-window')
@@ -300,7 +333,7 @@ def last_focused_window():
return window_by_index(-1)
-def window_by_index(idx):
+def window_by_index(idx: int) -> 'mainwindow.MainWindow':
"""Get the Nth opened window object."""
if not window_registry:
raise NoWindow()
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 1dc2193da..3e81d4a23 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
+# FIXME:typing Can we have less "# type: ignore" in here?
+
"""Misc. utilities related to Qt.
Module attributes:
@@ -116,7 +118,7 @@ def is_new_qtwebkit() -> bool:
pkg_resources.parse_version('538.1'))
-def is_single_process():
+def is_single_process() -> bool:
"""Check whether QtWebEngine is running in single-process mode."""
if objects.backend == usertypes.Backend.QtWebKit:
return False
@@ -199,7 +201,11 @@ def deserialize_stream(stream: QDataStream, obj: QObject) -> None:
@contextlib.contextmanager
-def savefile_open(filename, binary=False, encoding='utf-8'):
+def savefile_open(
+ filename: str,
+ binary: bool = False,
+ encoding: str = 'utf-8'
+) -> typing.Iterator[typing.Union['PyQIODevice', io.TextIOWrapper]]:
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
cancelled = False
@@ -209,9 +215,11 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
raise QtOSError(f)
if binary:
- new_f = PyQIODevice(f)
+ new_f = PyQIODevice(
+ f) # type: typing.Union[PyQIODevice, io.TextIOWrapper]
else:
- new_f = io.TextIOWrapper(PyQIODevice(f), encoding=encoding)
+ new_f = io.TextIOWrapper(PyQIODevice(f), # type: ignore
+ encoding=encoding)
yield new_f
@@ -268,7 +276,7 @@ class PyQIODevice(io.BufferedIOBase):
if not self.writable():
raise OSError("Trying to write to unwritable file!")
- def open(self, mode):
+ def open(self, mode: QIODevice.OpenMode) -> contextlib.closing:
"""Open the underlying device and ensure opening succeeded.
Raises OSError if opening failed.
@@ -289,10 +297,10 @@ class PyQIODevice(io.BufferedIOBase):
"""Close the underlying device."""
self.dev.close()
- def fileno(self):
+ def fileno(self) -> int:
raise io.UnsupportedOperation
- def seek(self, offset, whence=io.SEEK_SET):
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
self._check_open()
self._check_random()
if whence == io.SEEK_SET:
@@ -307,7 +315,9 @@ class PyQIODevice(io.BufferedIOBase):
if not ok:
raise QtOSError(self.dev, msg="seek failed!")
- def truncate(self, size=None):
+ return self.dev.pos()
+
+ def truncate(self, size: int = None) -> int:
raise io.UnsupportedOperation
@property
@@ -325,7 +335,7 @@ class PyQIODevice(io.BufferedIOBase):
def readable(self) -> bool:
return self.dev.isReadable()
- def readline(self, size=-1):
+ def readline(self, size: int = -1) -> QByteArray:
self._check_open()
self._check_readable()
@@ -346,7 +356,7 @@ class PyQIODevice(io.BufferedIOBase):
if buf is None:
raise QtOSError(self.dev)
- return buf
+ return buf # type: ignore
def seekable(self) -> bool:
return not self.dev.isSequential()
@@ -359,23 +369,26 @@ class PyQIODevice(io.BufferedIOBase):
def writable(self) -> bool:
return self.dev.isWritable()
- def write(self, b: bytes) -> int:
+ def write(self, data: str) -> int: # type: ignore
self._check_open()
self._check_writable()
- num = self.dev.write(b)
- if num == -1 or num < len(b):
+ num = self.dev.write(data) # type: ignore
+ if num == -1 or num < len(data):
raise QtOSError(self.dev)
return num
- def read(self, size=-1):
+ def read(self, size: typing.Optional[int] = None) -> QByteArray:
self._check_open()
self._check_readable()
- if size < 0:
+
+ if size in [None, -1]:
buf = self.dev.readAll()
else:
- buf = self.dev.read(size)
+ buf = self.dev.read(size) # type: ignore
+
if buf is None:
raise QtOSError(self.dev)
+
return buf
@@ -405,11 +418,14 @@ class EventLoop(QEventLoop):
super().__init__(parent)
self._executing = False
- def exec_(self, flags=QEventLoop.AllEvents):
+ def exec_(
+ self,
+ flags: QEventLoop.ProcessEventsFlag = QEventLoop.AllEvents
+ ) -> int:
"""Override exec_ to raise an exception when re-running."""
if self._executing:
raise AssertionError("Eventloop is already running!")
self._executing = True
- status = super().exec_(flags)
+ status = super().exec_(flags) # type: ignore
self._executing = False
return status
diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py
index 186a42ae3..e07ffe4c3 100644
--- a/qutebrowser/utils/standarddir.py
+++ b/qutebrowser/utils/standarddir.py
@@ -25,6 +25,8 @@ import sys
import shutil
import contextlib
import enum
+import argparse
+import typing
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
@@ -58,7 +60,7 @@ class EmptyValueError(Exception):
@contextlib.contextmanager
-def _unset_organization():
+def _unset_organization() -> typing.Iterator[None]:
"""Temporarily unset QApplication.organizationName().
This is primarily needed in config.py.
@@ -66,7 +68,7 @@ def _unset_organization():
qapp = QApplication.instance()
if qapp is not None:
orgname = qapp.organizationName()
- qapp.setOrganizationName(None)
+ qapp.setOrganizationName(None) # type: ignore
try:
yield
finally:
@@ -74,36 +76,38 @@ def _unset_organization():
qapp.setOrganizationName(orgname)
-def _init_config(args):
+def _init_config(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize the location for configs."""
typ = QStandardPaths.ConfigLocation
- overridden, path = _from_args(typ, args)
- if not overridden:
+ path = _from_args(typ, args)
+ if path is None:
if utils.is_windows:
app_data_path = _writable_location(
QStandardPaths.AppDataLocation)
path = os.path.join(app_data_path, 'config')
else:
path = _writable_location(typ)
+
_create(path)
_locations[_Location.config] = path
_locations[_Location.auto_config] = path
# Override the normal (non-auto) config on macOS
if utils.is_mac:
- overridden, path = _from_args(typ, args)
- if not overridden: # pragma: no branch
+ path = _from_args(typ, args)
+ if path is None: # pragma: no branch
path = os.path.expanduser('~/.' + APPNAME)
_create(path)
_locations[_Location.config] = path
config_py_file = os.path.join(_locations[_Location.config], 'config.py')
if getattr(args, 'config_py', None) is not None:
+ assert args is not None
config_py_file = os.path.abspath(args.config_py)
_locations[_Location.config_py] = config_py_file
-def config(auto=False):
+def config(auto: bool = False) -> str:
"""Get the location for the config directory.
If auto=True is given, get the location for the autoconfig.yml directory,
@@ -123,11 +127,11 @@ def config_py() -> str:
return _locations[_Location.config_py]
-def _init_data(args):
+def _init_data(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize the location for data."""
typ = QStandardPaths.DataLocation
- overridden, path = _from_args(typ, args)
- if not overridden:
+ path = _from_args(typ, args)
+ if path is None:
if utils.is_windows:
app_data_path = _writable_location(QStandardPaths.AppDataLocation)
path = os.path.join(app_data_path, 'data')
@@ -137,6 +141,7 @@ def _init_data(args):
path = os.path.join(config_path, 'data')
else:
path = _writable_location(typ)
+
_create(path)
_locations[_Location.data] = path
@@ -148,7 +153,7 @@ def _init_data(args):
_locations[_Location.system_data] = path
-def data(system=False):
+def data(system: bool = False) -> str:
"""Get the data directory.
If system=True is given, gets the system-wide (probably non-writable) data
@@ -162,43 +167,44 @@ def data(system=False):
return _locations[_Location.data]
-def _init_cache(args):
+def _init_cache(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize the location for the cache."""
typ = QStandardPaths.CacheLocation
- overridden, path = _from_args(typ, args)
- if not overridden:
+ path = _from_args(typ, args)
+ if path is None:
if utils.is_windows:
# Local, not Roaming!
data_path = _writable_location(QStandardPaths.DataLocation)
path = os.path.join(data_path, 'cache')
else:
path = _writable_location(typ)
+
_create(path)
_locations[_Location.cache] = path
-def cache():
+def cache() -> str:
return _locations[_Location.cache]
-def _init_download(args):
+def _init_download(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize the location for downloads.
Note this is only the default directory as found by Qt.
Therefore, we also don't create it.
"""
typ = QStandardPaths.DownloadLocation
- overridden, path = _from_args(typ, args)
- if not overridden:
+ path = _from_args(typ, args)
+ if path is None:
path = _writable_location(typ)
_locations[_Location.download] = path
-def download():
+def download() -> str:
return _locations[_Location.download]
-def _init_runtime(args):
+def _init_runtime(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize location for runtime data."""
if utils.is_mac or utils.is_windows:
# RuntimeLocation is a weird path on macOS and Windows.
@@ -206,9 +212,8 @@ def _init_runtime(args):
else:
typ = QStandardPaths.RuntimeLocation
- overridden, path = _from_args(typ, args)
-
- if not overridden:
+ path = _from_args(typ, args)
+ if path is None:
try:
path = _writable_location(typ)
except EmptyValueError:
@@ -231,11 +236,11 @@ def _init_runtime(args):
_locations[_Location.runtime] = path
-def runtime():
+def runtime() -> str:
return _locations[_Location.runtime]
-def _writable_location(typ):
+def _writable_location(typ: QStandardPaths.StandardLocation) -> str:
"""Wrapper around QStandardPaths.writableLocation.
Arguments:
@@ -271,17 +276,14 @@ def _writable_location(typ):
return path
-def _from_args(typ, args):
+def _from_args(
+ typ: QStandardPaths.StandardLocation,
+ args: typing.Optional[argparse.Namespace]
+) -> typing.Optional[str]:
"""Get the standard directory from an argparse namespace.
- Args:
- typ: A member of the QStandardPaths::StandardLocation enum
- args: An argparse namespace or None.
-
Return:
- A (override, path) tuple.
- override: boolean, if the user did override the path
- path: The overridden path, or None to turn off storage.
+ The overridden path, or None if there is no override.
"""
basedir_suffix = {
QStandardPaths.ConfigLocation: 'config',
@@ -291,19 +293,18 @@ def _from_args(typ, args):
QStandardPaths.RuntimeLocation: 'runtime',
}
- if getattr(args, 'basedir', None) is not None:
- basedir = args.basedir
+ if getattr(args, 'basedir', None) is None:
+ return None
+ assert args is not None
- try:
- suffix = basedir_suffix[typ]
- except KeyError: # pragma: no cover
- return (False, None)
- return (True, os.path.abspath(os.path.join(basedir, suffix)))
- else:
- return (False, None)
+ try:
+ suffix = basedir_suffix[typ]
+ except KeyError: # pragma: no cover
+ return None
+ return os.path.abspath(os.path.join(args.basedir, suffix))
-def _create(path):
+def _create(path: str) -> None:
"""Create the `path` directory.
From the XDG basedir spec:
@@ -315,7 +316,7 @@ def _create(path):
os.makedirs(path, 0o700, exist_ok=True)
-def _init_dirs(args=None):
+def _init_dirs(args: argparse.Namespace = None) -> None:
"""Create and cache standard directory locations.
Mainly in a separate function because we need to call it in tests.
@@ -327,7 +328,7 @@ def _init_dirs(args=None):
_init_runtime(args)
-def init(args):
+def init(args: typing.Optional[argparse.Namespace]) -> None:
"""Initialize all standard dirs."""
if args is not None:
# args can be None during tests
@@ -342,7 +343,7 @@ def init(args):
_move_windows()
-def _move_macos():
+def _move_macos() -> None:
"""Move most config files to new location on macOS."""
old_config = config(auto=True) # ~/Library/Preferences/qutebrowser
new_config = config() # ~/.qutebrowser
@@ -352,7 +353,7 @@ def _move_macos():
os.path.join(new_config, f))
-def _move_windows():
+def _move_windows() -> None:
"""Move the whole qutebrowser directory from Local to Roaming AppData."""
# %APPDATA%\Local\qutebrowser
old_appdata_dir = _writable_location(QStandardPaths.DataLocation)
@@ -375,7 +376,7 @@ def _move_windows():
os.path.join(new_config_dir, f))
-def _init_cachedir_tag():
+def _init_cachedir_tag() -> None:
"""Create CACHEDIR.TAG if it doesn't exist.
See http://www.brynosaurus.com/cachedir/spec.html
@@ -394,7 +395,7 @@ def _init_cachedir_tag():
log.init.exception("Failed to create CACHEDIR.TAG")
-def _move_data(old, new):
+def _move_data(old: str, new: str) -> bool:
"""Migrate data from an old to a new directory.
If the old directory does not exist, the migration is skipped.
diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py
index db7adf139..4a4e68189 100644
--- a/qutebrowser/utils/urlmatch.py
+++ b/qutebrowser/utils/urlmatch.py
@@ -27,6 +27,7 @@ https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
import ipaddress
import fnmatch
+import typing
import urllib.parse
from PyQt5.QtCore import QUrl
@@ -64,15 +65,15 @@ class UrlPattern:
_DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21}
_SCHEMES_WITHOUT_HOST = ['about', 'file', 'data', 'javascript']
- def __init__(self, pattern):
+ def __init__(self, pattern: str) -> None:
# Make sure all attributes are initialized if we exit early.
self._pattern = pattern
self._match_all = False
self._match_subdomains = False
- self._scheme = None
- self._host = None
- self._path = None
- self._port = None
+ self._scheme = None # type: typing.Optional[str]
+ self._host = None # type: typing.Optional[str]
+ self._path = None # type: typing.Optional[str]
+ self._port = None # type: typing.Optional[int]
# > The special pattern matches any URL that starts with a
# > permitted scheme.
@@ -99,26 +100,26 @@ class UrlPattern:
self._init_path(parsed)
self._init_port(parsed)
- def _to_tuple(self):
+ def _to_tuple(self) -> typing.Tuple:
"""Get a pattern with information used for __eq__/__hash__."""
return (self._match_all, self._match_subdomains, self._scheme,
self._host, self._path, self._port)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash(self._to_tuple())
- def __eq__(self, other):
+ def __eq__(self, other: typing.Any) -> bool:
if not isinstance(other, UrlPattern):
return NotImplemented
return self._to_tuple() == other._to_tuple()
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, pattern=self._pattern, constructor=True)
- def __str__(self):
+ def __str__(self) -> str:
return self._pattern
- def _fixup_pattern(self, pattern):
+ def _fixup_pattern(self, pattern: str) -> str:
"""Make sure the given pattern is parseable by urllib.parse."""
if pattern.startswith('*:'): # Any scheme, but *:// is unparseable
pattern = 'any:' + pattern[2:]
@@ -135,7 +136,7 @@ class UrlPattern:
return pattern
- def _init_scheme(self, parsed):
+ def _init_scheme(self, parsed: urllib.parse.ParseResult) -> None:
"""Parse the scheme from the given URL.
Deviation from Chromium:
@@ -150,7 +151,7 @@ class UrlPattern:
self._scheme = parsed.scheme
- def _init_path(self, parsed):
+ def _init_path(self, parsed: urllib.parse.ParseResult) -> None:
"""Parse the path from the given URL.
Deviation from Chromium:
@@ -168,13 +169,15 @@ class UrlPattern:
else:
self._path = parsed.path
- def _init_host(self, parsed):
+ def _init_host(self, parsed: urllib.parse.ParseResult) -> None:
"""Parse the host from the given URL.
Deviation from Chromium:
- http://:1234/ is not a valid URL because it has no host.
"""
- if parsed.hostname is None or not parsed.hostname.strip():
+ # https://github.com/python/typeshed/commit/f0ccb325aa787ca0a539ef9914276b2c3148327a
+ if (parsed.hostname is None or # type: ignore
+ not parsed.hostname.strip()):
if self._scheme not in self._SCHEMES_WITHOUT_HOST:
raise ParseError("Pattern without host")
assert self._host is None
@@ -208,7 +211,7 @@ class UrlPattern:
# Only * or *.foo is allowed as host.
raise ParseError("Invalid host wildcard")
- def _init_port(self, parsed):
+ def _init_port(self, parsed: urllib.parse.ParseResult) -> None:
"""Parse the port from the given URL.
Deviation from Chromium:
@@ -225,15 +228,16 @@ class UrlPattern:
except ValueError as e:
raise ParseError("Invalid port: {}".format(e))
- if (self._scheme not in list(self._DEFAULT_PORTS) + [None] and
- self._port is not None):
+ scheme_has_port = (self._scheme in list(self._DEFAULT_PORTS) or
+ self._scheme is None)
+ if self._port is not None and not scheme_has_port:
raise ParseError("Ports are unsupported with {} scheme".format(
self._scheme))
- def _matches_scheme(self, scheme):
+ def _matches_scheme(self, scheme: str) -> bool:
return self._scheme is None or self._scheme == scheme
- def _matches_host(self, host):
+ def _matches_host(self, host: str) -> bool:
# FIXME what about multiple dots?
host = host.rstrip('.')
@@ -268,12 +272,12 @@ class UrlPattern:
return host[len(host) - len(self._host) - 1] == '.'
- def _matches_port(self, scheme, port):
+ def _matches_port(self, scheme: str, port: int) -> bool:
if port == -1 and scheme in self._DEFAULT_PORTS:
port = self._DEFAULT_PORTS[scheme]
return self._port is None or self._port == port
- def _matches_path(self, path):
+ def _matches_path(self, path: str) -> bool:
if self._path is None:
return True
@@ -285,7 +289,7 @@ class UrlPattern:
# doesn't rely on regexes. Do we need that too?
return fnmatch.fnmatchcase(path, self._path)
- def matches(self, qurl):
+ def matches(self, qurl: QUrl) -> bool:
"""Check if the pattern matches the given QUrl."""
qtutils.ensure_valid(qurl)
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index c6f4cb858..74273bd85 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -25,6 +25,7 @@ import os.path
import ipaddress
import posixpath
import urllib.parse
+import typing
from PyQt5.QtCore import QUrl, QUrlQuery
from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy
@@ -58,7 +59,7 @@ class InvalidUrlError(Exception):
"""Error raised if a function got an invalid URL."""
- def __init__(self, url):
+ def __init__(self, url: QUrl) -> None:
if url.isValid():
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
self.url = url
@@ -66,7 +67,7 @@ class InvalidUrlError(Exception):
super().__init__(self.msg)
-def _parse_search_term(s):
+def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str], str]:
"""Get a search engine name and search term from a string.
Args:
@@ -79,7 +80,7 @@ def _parse_search_term(s):
split = s.split(maxsplit=1)
if len(split) == 2:
- engine = split[0]
+ engine = split[0] # type: typing.Optional[str]
try:
config.val.url.searchengines[engine]
except KeyError:
@@ -97,7 +98,7 @@ def _parse_search_term(s):
return (engine, term)
-def _get_search_url(txt):
+def _get_search_url(txt: str) -> QUrl:
"""Get a search engine URL for a text.
Args:
@@ -117,14 +118,14 @@ def _get_search_url(txt):
if config.val.url.open_base_url and term in config.val.url.searchengines:
url = qurl_from_user_input(config.val.url.searchengines[term])
- url.setPath(None)
- url.setFragment(None)
- url.setQuery(None)
+ url.setPath(None) # type: ignore
+ url.setFragment(None) # type: ignore
+ url.setQuery(None) # type: ignore
qtutils.ensure_valid(url)
return url
-def _is_url_naive(urlstr):
+def _is_url_naive(urlstr: str) -> bool:
"""Naive check if given URL is really a URL.
Args:
@@ -150,7 +151,7 @@ def _is_url_naive(urlstr):
return '.' in host and not host.endswith('.')
-def _is_url_dns(urlstr):
+def _is_url_dns(urlstr: str) -> bool:
"""Check if a URL is really a URL via DNS.
Args:
@@ -178,8 +179,11 @@ def _is_url_dns(urlstr):
return not info.error()
-def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True,
- force_search=False):
+def fuzzy_url(urlstr: str,
+ cwd: str = None,
+ relative: bool = False,
+ do_search: bool = True,
+ force_search: bool = False) -> QUrl:
"""Get a QUrl based on a user input which is URL or search term.
Args:
@@ -218,7 +222,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True,
return url
-def _has_explicit_scheme(url):
+def _has_explicit_scheme(url: QUrl) -> bool:
"""Check if a url has an explicit scheme given.
Args:
@@ -228,13 +232,13 @@ def _has_explicit_scheme(url):
# after the scheme delimiter. Since we don't know of any URIs
# using this and want to support e.g. searching for scoped C++
# symbols, we treat this as not a URI anyways.
- return (url.isValid() and url.scheme() and
- (url.host() or url.path()) and
- ' ' not in url.path() and
- not url.path().startswith(':'))
+ return bool(url.isValid() and url.scheme() and
+ (url.host() or url.path()) and
+ ' ' not in url.path() and
+ not url.path().startswith(':'))
-def is_special_url(url):
+def is_special_url(url: QUrl) -> bool:
"""Return True if url is an about:... or other special URL.
Args:
@@ -246,7 +250,7 @@ def is_special_url(url):
return url.scheme() in special_schemes
-def is_url(urlstr):
+def is_url(urlstr: str) -> bool:
"""Check if url seems to be a valid URL.
Args:
@@ -303,7 +307,7 @@ def is_url(urlstr):
return url
-def qurl_from_user_input(urlstr):
+def qurl_from_user_input(urlstr: str) -> QUrl:
"""Get a QUrl based on a user input. Additionally handles IPv6 addresses.
QUrl.fromUserInput handles something like '::1' as a file URL instead of an
@@ -339,12 +343,12 @@ def qurl_from_user_input(urlstr):
return QUrl('http://[{}]{}'.format(ipstr, rest))
-def ensure_valid(url):
+def ensure_valid(url: QUrl) -> None:
if not url.isValid():
raise InvalidUrlError(url)
-def invalid_url_error(url, action):
+def invalid_url_error(url: QUrl, action: str) -> None:
"""Display an error message for a URL.
Args:
@@ -358,7 +362,7 @@ def invalid_url_error(url, action):
message.error(errstring)
-def raise_cmdexc_if_invalid(url):
+def raise_cmdexc_if_invalid(url: QUrl) -> None:
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
try:
ensure_valid(url)
@@ -366,7 +370,10 @@ def raise_cmdexc_if_invalid(url):
raise cmdutils.CommandError(str(e))
-def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
+def get_path_if_valid(pathstr: str,
+ cwd: str = None,
+ relative: bool = False,
+ check_exists: bool = False) -> typing.Optional[str]:
"""Check if path is a valid path.
Args:
@@ -384,7 +391,7 @@ def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
expanded = os.path.expanduser(pathstr)
if os.path.isabs(expanded):
- path = expanded
+ path = expanded # type: typing.Optional[str]
elif relative and cwd:
path = os.path.join(cwd, expanded)
elif relative:
@@ -411,7 +418,7 @@ def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
return path
-def filename_from_url(url):
+def filename_from_url(url: QUrl) -> typing.Optional[str]:
"""Get a suitable filename from a URL.
Args:
@@ -431,7 +438,7 @@ def filename_from_url(url):
return None
-def host_tuple(url):
+def host_tuple(url: QUrl) -> typing.Tuple[str, str, int]:
"""Get a (scheme, host, port) tuple from a QUrl.
This is suitable to identify a connection, e.g. for SSL errors.
@@ -456,7 +463,7 @@ def host_tuple(url):
return scheme, host, port
-def get_errstring(url, base="Invalid URL"):
+def get_errstring(url: QUrl, base: str = "Invalid URL") -> str:
"""Get an error string for a URL.
Args:
@@ -473,7 +480,7 @@ def get_errstring(url, base="Invalid URL"):
return base
-def same_domain(url1, url2):
+def same_domain(url1: QUrl, url2: QUrl) -> bool:
"""Check if url1 and url2 belong to the same website.
This will use a "public suffix list" to determine what a "top level domain"
@@ -501,7 +508,7 @@ def same_domain(url1, url2):
return domain1 == domain2
-def encoded_url(url):
+def encoded_url(url: QUrl) -> str:
"""Return the fully encoded url as string.
Args:
@@ -510,16 +517,17 @@ def encoded_url(url):
return bytes(url.toEncoded()).decode('ascii')
-def file_url(path):
+def file_url(path: str) -> QUrl:
"""Return a file:// url (as string) to the given local path.
Arguments:
path: The absolute path to the local file
"""
- return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
+ url = QUrl.fromLocalFile(path)
+ return url.toString(QUrl.FullyEncoded) # type: ignore
-def data_url(mimetype, data):
+def data_url(mimetype: str, data: bytes) -> QUrl:
"""Get a data: QUrl for the given data."""
b64 = base64.b64encode(data).decode('ascii')
url = QUrl('data:{};base64,{}'.format(mimetype, b64))
@@ -527,7 +535,7 @@ def data_url(mimetype, data):
return url
-def safe_display_string(qurl):
+def safe_display_string(qurl: QUrl) -> str:
"""Get a IDN-homograph phishing safe form of the given QUrl.
If we're dealing with a Punycode-encoded URL, this prepends the hostname in
@@ -538,19 +546,20 @@ def safe_display_string(qurl):
"""
ensure_valid(qurl)
- host = qurl.host(QUrl.FullyEncoded)
+ host = qurl.host(QUrl.FullyEncoded) # type: ignore
if '..' in host: # pragma: no cover
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60364
return '(unparseable URL!) {}'.format(qurl.toDisplayString())
for part in host.split('.'):
- if part.startswith('xn--') and host != qurl.host(QUrl.FullyDecoded):
+ url_host = qurl.host(QUrl.FullyDecoded) # type: ignore
+ if part.startswith('xn--') and host != url_host:
return '({}) {}'.format(host, qurl.toDisplayString())
return qurl.toDisplayString()
-def query_string(qurl):
+def query_string(qurl: QUrl) -> str:
"""Get a query string for the given URL.
This is a WORKAROUND for:
@@ -566,11 +575,11 @@ class InvalidProxyTypeError(Exception):
"""Error raised when proxy_from_url gets an unknown proxy type."""
- def __init__(self, typ):
+ def __init__(self, typ: str) -> None:
super().__init__("Invalid proxy type {}!".format(typ))
-def proxy_from_url(url):
+def proxy_from_url(url: QUrl) -> QNetworkProxy:
"""Create a QNetworkProxy from QUrl and a proxy type.
Args:
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 0bb7badc8..102cc01e9 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -17,23 +17,31 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
-"""Custom useful data types.
-
-Module attributes:
- _UNSET: Used as default argument in the constructor so default can be None.
-"""
+"""Custom useful data types."""
import operator
import collections.abc
import enum
+import typing
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
+from PyQt5.QtCore import QUrl # pylint: disable=unused-import
from qutebrowser.utils import log, qtutils, utils
-_UNSET = object()
+_T = typing.TypeVar('_T')
+
+
+class UnsetObject:
+
+ """Class for an unset object."""
+
+ __slots__ = ()
+
+
+UNSET = UnsetObject()
class NeighborList(collections.abc.Sequence):
@@ -52,7 +60,9 @@ class NeighborList(collections.abc.Sequence):
Modes = enum.Enum('Modes', ['edge', 'exception'])
- def __init__(self, items=None, default=_UNSET, mode=Modes.exception):
+ def __init__(self, items: typing.Sequence[_T] = None,
+ default: typing.Union[_T, UnsetObject] = UNSET,
+ mode: Modes = Modes.exception) -> None:
"""Constructor.
Args:
@@ -65,28 +75,31 @@ class NeighborList(collections.abc.Sequence):
if not isinstance(mode, self.Modes):
raise TypeError("Mode {} is not a Modes member!".format(mode))
if items is None:
- self._items = []
+ self._items = [] # type: typing.Sequence[_T]
else:
self._items = list(items)
self._default = default
- if default is not _UNSET:
- self._idx = self._items.index(default)
+
+ if not isinstance(default, UnsetObject):
+ idx = self._items.index(default)
+ self._idx = idx # type: typing.Optional[int]
else:
self._idx = None
- self._mode = mode
- self.fuzzyval = None
- def __getitem__(self, key):
+ self._mode = mode
+ self.fuzzyval = None # type: typing.Optional[int]
+
+ def __getitem__(self, key: int) -> _T: # type: ignore
return self._items[key]
- def __len__(self):
+ def __len__(self) -> int:
return len(self._items)
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, items=self._items, mode=self._mode,
idx=self._idx, fuzzyval=self.fuzzyval)
- def _snap_in(self, offset):
+ def _snap_in(self, offset: int) -> bool:
"""Set the current item to the closest item to self.fuzzyval.
Args:
@@ -97,11 +110,15 @@ class NeighborList(collections.abc.Sequence):
True if the value snapped in (changed),
False when the value already was in the list.
"""
+ assert isinstance(self.fuzzyval, (int, float)), self.fuzzyval
+
op = operator.le if offset < 0 else operator.ge
items = [(idx, e) for (idx, e) in enumerate(self._items)
if op(e, self.fuzzyval)]
if items:
- item = min(items, key=lambda tpl: abs(self.fuzzyval - tpl[1]))
+ item = min(
+ items,
+ key=lambda tpl: abs(self.fuzzyval - tpl[1])) # type: ignore
else:
sorted_items = sorted(((idx, e) for (idx, e) in
enumerate(self.items)), key=lambda e: e[1])
@@ -110,7 +127,7 @@ class NeighborList(collections.abc.Sequence):
self._idx = item[0]
return self.fuzzyval not in self._items
- def _get_new_item(self, offset):
+ def _get_new_item(self, offset: int) -> _T:
"""Logic for getitem to get the item at offset.
Args:
@@ -119,6 +136,7 @@ class NeighborList(collections.abc.Sequence):
Return:
The new item.
"""
+ assert self._idx is not None
try:
if self._idx + offset >= 0:
new = self._items[self._idx + offset]
@@ -138,11 +156,11 @@ class NeighborList(collections.abc.Sequence):
return new
@property
- def items(self):
+ def items(self) -> typing.Sequence[_T]:
"""Getter for items, which should not be set."""
return self._items
- def getitem(self, offset):
+ def getitem(self, offset: int) -> _T:
"""Get the item with a relative position.
Args:
@@ -167,38 +185,38 @@ class NeighborList(collections.abc.Sequence):
self.fuzzyval = None
return self._get_new_item(offset)
- def curitem(self):
+ def curitem(self) -> _T:
"""Get the current item in the list."""
if self._idx is not None:
return self._items[self._idx]
else:
raise IndexError("No current item!")
- def nextitem(self):
+ def nextitem(self) -> _T:
"""Get the next item in the list."""
return self.getitem(1)
- def previtem(self):
+ def previtem(self) -> _T:
"""Get the previous item in the list."""
return self.getitem(-1)
- def firstitem(self):
+ def firstitem(self) -> _T:
"""Get the first item in the list."""
if not self._items:
raise IndexError("No items found!")
self._idx = 0
return self.curitem()
- def lastitem(self):
+ def lastitem(self) -> _T:
"""Get the last item in the list."""
if not self._items:
raise IndexError("No items found!")
self._idx = len(self._items) - 1
return self.curitem()
- def reset(self):
+ def reset(self) -> _T:
"""Reset the position to the default."""
- if self._default is _UNSET:
+ if self._default is UNSET:
raise ValueError("No default set!")
self._idx = self._items.index(self._default)
return self.curitem()
@@ -334,37 +352,25 @@ class Question(QObject):
answered_no = pyqtSignal()
completed = pyqtSignal()
- def __init__(self, parent=None):
+ def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
- self._mode = None
- self.default = None
- self.title = None
- self.text = None
- self.url = None
- self.option = None
- self.answer = None
+ self.mode = None # type: typing.Optional[PromptMode]
+ self.default = None # type: typing.Union[bool, str, None]
+ self.title = None # type: typing.Optional[str]
+ self.text = None # type: typing.Optional[str]
+ self.url = None # type: typing.Optional[QUrl]
+ self.option = None # type: typing.Optional[bool]
+ self.answer = None # type: typing.Union[str, bool, None]
self.is_aborted = False
self.interrupted = False
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, title=self.title, text=self.text,
- mode=self._mode, default=self.default,
+ mode=self.mode, default=self.default,
option=self.option)
- @property
- def mode(self):
- """Getter for mode so we can define a setter."""
- return self._mode
-
- @mode.setter
- def mode(self, val):
- """Setter for mode to do basic type checking."""
- if not isinstance(val, PromptMode):
- raise TypeError("Mode {} is no PromptMode member!".format(val))
- self._mode = val
-
@pyqtSlot()
- def done(self):
+ def done(self) -> None:
"""Must be called when the question was answered completely."""
self.answered.emit(self.answer)
if self.mode == PromptMode.yesno:
@@ -375,13 +381,13 @@ class Question(QObject):
self.completed.emit()
@pyqtSlot()
- def cancel(self):
+ def cancel(self) -> None:
"""Cancel the question (resulting from user-input)."""
self.cancelled.emit()
self.completed.emit()
@pyqtSlot()
- def abort(self):
+ def abort(self) -> None:
"""Abort the question."""
if self.is_aborted:
log.misc.debug("Question was already aborted")
@@ -399,7 +405,7 @@ class Timer(QTimer):
_name: The name of the timer.
"""
- def __init__(self, parent=None, name=None):
+ def __init__(self, parent: QObject = None, name: str = None) -> None:
super().__init__(parent)
if name is None:
self._name = "unnamed"
@@ -407,15 +413,15 @@ class Timer(QTimer):
self.setObjectName(name)
self._name = name
- def __repr__(self):
+ def __repr__(self) -> str:
return utils.get_repr(self, name=self._name)
- def setInterval(self, msec):
+ def setInterval(self, msec: int) -> None:
"""Extend setInterval to check for overflows."""
qtutils.check_overflow(msec, 'int')
super().setInterval(msec)
- def start(self, msec=None):
+ def start(self, msec: int = None) -> None:
"""Extend start to check for overflows."""
if msec is not None:
qtutils.check_overflow(msec, 'int')
@@ -428,16 +434,16 @@ class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
- def __init__(self, error):
+ def __init__(self, error: typing.Any) -> None:
self._error = error
- def __str__(self):
+ def __str__(self) -> str:
raise NotImplementedError
- def __repr__(self):
+ def __repr__(self) -> str:
raise NotImplementedError
- def is_overridable(self):
+ def is_overridable(self) -> bool:
raise NotImplementedError
@@ -457,7 +463,7 @@ class NavigationRequest:
'other'
])
- url = attr.ib()
- navigation_type = attr.ib()
- is_main_frame = attr.ib()
- accepted = attr.ib(default=True)
+ url = attr.ib() # type: QUrl
+ navigation_type = attr.ib() # type: Type
+ is_main_frame = attr.ib() # type: bool
+ accepted = attr.ib(default=True) # type: bool
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 30fdaffb9..966515df0 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -35,6 +35,7 @@ import socket
import shlex
import glob
import mimetypes
+import typing
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
@@ -78,7 +79,7 @@ class SelectionUnsupportedError(ClipboardError):
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
- def __init__(self):
+ def __init__(self) -> None:
super().__init__("Primary selection is not supported on this "
"platform!")
@@ -88,7 +89,7 @@ class ClipboardEmptyError(ClipboardError):
"""Raised if get_clipboard is used and the clipboard is empty."""
-def elide(text, length):
+def elide(text: str, length: int) -> str:
"""Elide text so it uses a maximum of length chars."""
if length < 1:
raise ValueError("length must be >= 1!")
@@ -98,7 +99,7 @@ def elide(text, length):
return text[:length - 1] + '\u2026'
-def elide_filename(filename, length):
+def elide_filename(filename: str, length: int) -> str:
"""Elide a filename to the given length.
The difference to the elide() is that the text is removed from
@@ -130,7 +131,7 @@ def elide_filename(filename, length):
return filename[:left] + elidestr + filename[-right:]
-def compact_text(text, elidelength=None):
+def compact_text(text: str, elidelength: int = None) -> str:
"""Remove leading whitespace and newlines from a text and maybe elide it.
Args:
@@ -146,7 +147,7 @@ def compact_text(text, elidelength=None):
return out
-def preload_resources():
+def preload_resources() -> None:
"""Load resource files into the cache."""
for subdir, pattern in [('html', '*.html'), ('javascript', '*.js')]:
path = resource_filename(subdir)
@@ -155,7 +156,8 @@ def preload_resources():
_resource_cache[sub_path] = read_file(sub_path)
-def read_file(filename, binary=False):
+# FIXME:typing Return value should be bytes/str
+def read_file(filename: str, binary: bool = False) -> typing.Any:
"""Get the contents of a file contained with qutebrowser.
Args:
@@ -177,19 +179,22 @@ def read_file(filename, binary=False):
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc
fn = os.path.join(os.path.dirname(sys.executable), filename)
if binary:
- with open(fn, 'rb') as f:
+ with open(fn, 'rb') as f: # type: typing.IO
return f.read()
else:
with open(fn, 'r', encoding='utf-8') as f:
return f.read()
else:
- data = pkg_resources.resource_string(qutebrowser.__name__, filename)
- if not binary:
- data = data.decode('UTF-8')
- return data
+ data = pkg_resources.resource_string(
+ qutebrowser.__name__, filename)
+
+ if binary:
+ return data
+
+ return data.decode('UTF-8')
-def resource_filename(filename):
+def resource_filename(filename: str) -> str:
"""Get the absolute filename of a file contained with qutebrowser.
Args:
@@ -203,7 +208,9 @@ def resource_filename(filename):
return pkg_resources.resource_filename(qutebrowser.__name__, filename)
-def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
+def _get_color_percentage(a_c1: int, a_c2: int, a_c3:
+ int, b_c1: int, b_c2: int, b_c3: int,
+ percent: int) -> typing.Tuple[int, int, int]:
"""Get a color which is percent% interpolated between start and end.
Args:
@@ -224,7 +231,12 @@ def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
return (out_c1, out_c2, out_c3)
-def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
+def interpolate_color(
+ start: QColor,
+ end: QColor,
+ percent: int,
+ colorspace: typing.Optional[QColor.Spec] = QColor.Rgb
+) -> QColor:
"""Get an interpolated color value.
Args:
@@ -273,7 +285,7 @@ def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
return out
-def format_seconds(total_seconds):
+def format_seconds(total_seconds: int) -> str:
"""Format a count of seconds to get a [H:]M:SS string."""
prefix = '-' if total_seconds < 0 else ''
hours, rem = divmod(abs(round(total_seconds)), 3600)
@@ -289,7 +301,9 @@ def format_seconds(total_seconds):
return prefix + ':'.join(chunks)
-def format_size(size, base=1024, suffix=''):
+def format_size(size: typing.Optional[float],
+ base: int = 1024,
+ suffix: str = '') -> str:
"""Format a byte size so it's human readable.
Inspired by http://stackoverflow.com/q/1094841
@@ -308,13 +322,13 @@ class FakeIOStream(io.TextIOBase):
"""A fake file-like stream which calls a function for write-calls."""
- def __init__(self, write_func):
+ def __init__(self, write_func: typing.Callable[[str], int]) -> None:
super().__init__()
- self.write = write_func
+ self.write = write_func # type: ignore
@contextlib.contextmanager
-def fake_io(write_func):
+def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
"""Run code with stdout and stderr replaced by FakeIOStreams.
Args:
@@ -324,21 +338,21 @@ def fake_io(write_func):
old_stderr = sys.stderr
fake_stderr = FakeIOStream(write_func)
fake_stdout = FakeIOStream(write_func)
- sys.stderr = fake_stderr
- sys.stdout = fake_stdout
+ sys.stderr = fake_stderr # type: ignore
+ sys.stdout = fake_stdout # type: ignore
try:
yield
finally:
# If the code we did run did change sys.stdout/sys.stderr, we leave it
# unchanged. Otherwise, we reset it.
- if sys.stdout is fake_stdout:
+ if sys.stdout is fake_stdout: # type: ignore
sys.stdout = old_stdout
- if sys.stderr is fake_stderr:
+ if sys.stderr is fake_stderr: # type: ignore
sys.stderr = old_stderr
@contextlib.contextmanager
-def disabled_excepthook():
+def disabled_excepthook() -> typing.Iterator[None]:
"""Run code with the exception hook temporarily disabled."""
old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__
@@ -371,7 +385,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
_predicate: The condition which needs to be True to prevent exceptions
"""
- def __init__(self, retval, predicate=True):
+ def __init__(self, retval: typing.Any, predicate: bool = True) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
@@ -382,7 +396,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
self._retval = retval
self._predicate = predicate
- def __call__(self, func):
+ def __call__(self, func: typing.Callable) -> typing.Callable:
"""Called when a function should be decorated.
Args:
@@ -397,7 +411,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
retval = self._retval
@functools.wraps(func)
- def wrapper(*args, **kwargs):
+ def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
"""Call the original function."""
try:
return func(*args, **kwargs)
@@ -408,7 +422,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
return wrapper
-def is_enum(obj):
+def is_enum(obj: typing.Any) -> bool:
"""Check if a given object is an enum."""
try:
return issubclass(obj, enum.Enum)
@@ -416,7 +430,9 @@ def is_enum(obj):
return False
-def get_repr(obj, constructor=False, **attrs):
+def get_repr(obj: typing.Any,
+ constructor: bool = False,
+ **attrs: typing.Any) -> str:
"""Get a suitable __repr__ string for an object.
Args:
@@ -439,7 +455,7 @@ def get_repr(obj, constructor=False, **attrs):
return '<{}>'.format(cls)
-def qualname(obj):
+def qualname(obj: typing.Any) -> str:
"""Get the fully qualified name of an object.
Based on twisted.python.reflect.fullyQualifiedName.
@@ -467,7 +483,10 @@ def qualname(obj):
return repr(obj)
-def raises(exc, func, *args):
+def raises(exc: typing.Union[typing.Type[BaseException],
+ typing.Tuple[typing.Type[BaseException]]],
+ func: typing.Callable,
+ *args: typing.Any) -> bool:
"""Check if a function raises a given exception.
Args:
@@ -486,7 +505,7 @@ def raises(exc, func, *args):
return False
-def force_encoding(text, encoding):
+def force_encoding(text: str, encoding: str) -> str:
"""Make sure a given text is encodable with the given encoding.
This replaces all chars not encodable with question marks.
@@ -494,7 +513,8 @@ def force_encoding(text, encoding):
return text.encode(encoding, errors='replace').decode(encoding)
-def sanitize_filename(name, replacement='_'):
+def sanitize_filename(name: str,
+ replacement: typing.Optional[str] = '_') -> str:
"""Replace invalid filename characters.
Note: This should be used for the basename, as it also removes the path
@@ -527,7 +547,7 @@ def sanitize_filename(name, replacement='_'):
return name
-def set_clipboard(data, selection=False):
+def set_clipboard(data: str, selection: bool = False) -> None:
"""Set the clipboard to some given data."""
global fake_clipboard
if selection and not supports_selection():
@@ -541,7 +561,7 @@ def set_clipboard(data, selection=False):
QApplication.clipboard().setText(data, mode=mode)
-def get_clipboard(selection=False, fallback=False):
+def get_clipboard(selection: bool = False, fallback: bool = False) -> str:
"""Get data from the clipboard.
Args:
@@ -574,12 +594,12 @@ def get_clipboard(selection=False, fallback=False):
return data
-def supports_selection():
+def supports_selection() -> bool:
"""Check if the OS supports primary selection."""
return QApplication.clipboard().supportsSelection()
-def random_port():
+def random_port() -> int:
"""Get a random free port."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', 0))
@@ -588,7 +608,7 @@ def random_port():
return port
-def open_file(filename, cmdline=None):
+def open_file(filename: str, cmdline: str = None) -> None:
"""Open the given file.
If cmdline is not given, downloads.open_dispatcher is used.
@@ -624,6 +644,8 @@ def open_file(filename, cmdline=None):
if cmdline is None and override:
cmdline = override
+ assert cmdline is not None
+
cmd, *args = shlex.split(cmdline)
args = [arg.replace('{}', filename) for arg in args]
if '{}' not in cmdline:
@@ -634,11 +656,11 @@ def open_file(filename, cmdline=None):
proc.start_detached(cmd, args)
-def unused(_arg):
+def unused(_arg: typing.Any) -> None:
"""Function which does nothing to avoid pylint complaining."""
-def expand_windows_drive(path):
+def expand_windows_drive(path: str) -> str:
r"""Expand a drive-path like E: into E:\.
Does nothing for other paths.
@@ -656,7 +678,7 @@ def expand_windows_drive(path):
return path
-def yaml_load(f):
+def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
"""Wrapper over yaml.load using the C loader if possible."""
start = datetime.datetime.now()
@@ -686,7 +708,8 @@ def yaml_load(f):
return data
-def yaml_dump(data, f=None):
+def yaml_dump(data: typing.Any,
+ f: typing.IO[str] = None) -> typing.Optional[str]:
"""Wrapper over yaml.dump using the C dumper if possible.
Also returns a str instead of bytes.
@@ -699,7 +722,7 @@ def yaml_dump(data, f=None):
return yaml_data.decode('utf-8')
-def chunk(elems, n):
+def chunk(elems: typing.Sequence, n: int) -> typing.Iterator[typing.Sequence]:
"""Yield successive n-sized chunks from elems.
If elems % n != 0, the last chunk will be smaller.
@@ -710,7 +733,7 @@ def chunk(elems, n):
yield elems[i:i + n]
-def guess_mimetype(filename, fallback=False):
+def guess_mimetype(filename: str, fallback: bool = False) -> str:
"""Guess a mimetype based on a filename.
Args:
@@ -726,7 +749,7 @@ def guess_mimetype(filename, fallback=False):
return mimetype
-def ceil_log(number, base):
+def ceil_log(number: int, base: int) -> int:
"""Compute max(1, ceil(log(number, base))).
Use only integer arithmetic in order to avoid numerical error.
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 3d784dd91..b339994a8 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -30,6 +30,7 @@ import collections
import enum
import datetime
import getpass
+import typing
import attr
import pkg_resources
@@ -66,10 +67,10 @@ class DistributionInfo:
"""Information about the running distribution."""
- id = attr.ib()
- parsed = attr.ib()
- version = attr.ib()
- pretty = attr.ib()
+ id = attr.ib() # type: typing.Optional[str]
+ parsed = attr.ib() # type: Distribution
+ version = attr.ib() # type: typing.Optional[typing.Tuple[str, ...]]
+ pretty = attr.ib() # type: str
pastebin_url = None
@@ -79,7 +80,7 @@ Distribution = enum.Enum(
'kde'])
-def distribution():
+def distribution() -> typing.Optional[DistributionInfo]:
"""Get some information about the running Linux distribution.
Returns:
@@ -104,9 +105,12 @@ def distribution():
pretty = info.get('PRETTY_NAME', None)
if pretty in ['Linux', None]: # Funtoo has PRETTY_NAME=Linux
pretty = info.get('NAME', 'Unknown')
+ assert pretty is not None
if 'VERSION_ID' in info:
- dist_version = pkg_resources.parse_version(info['VERSION_ID'])
+ dist_version = pkg_resources.parse_version(
+ info['VERSION_ID']
+ ) # type: typing.Optional[typing.Tuple[str, ...]]
else:
dist_version = None
@@ -115,16 +119,19 @@ def distribution():
'funtoo': 'gentoo', # does not have ID_LIKE=gentoo
'org.kde.Platform': 'kde',
}
- try:
- parsed = Distribution[id_mappings.get(dist_id, dist_id)]
- except KeyError:
- parsed = Distribution.unknown
+
+ parsed = Distribution.unknown
+ if dist_id is not None:
+ try:
+ parsed = Distribution[id_mappings.get(dist_id, dist_id)]
+ except KeyError:
+ pass
return DistributionInfo(parsed=parsed, version=dist_version, pretty=pretty,
id=dist_id)
-def _git_str():
+def _git_str() -> typing.Optional[str]:
"""Try to find out git version.
Return:
@@ -150,7 +157,7 @@ def _git_str():
return None
-def _git_str_subprocess(gitpath):
+def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
"""Try to get the git commit ID and timestamp by calling git.
Args:
@@ -176,7 +183,7 @@ def _git_str_subprocess(gitpath):
return None
-def _release_info():
+def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
"""Try to gather distribution release information.
Return:
@@ -200,7 +207,7 @@ def _release_info():
return data
-def _module_versions():
+def _module_versions() -> typing.Sequence[str]:
"""Get versions of optional modules.
Return:
@@ -219,7 +226,7 @@ def _module_versions():
('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),
('PyQt5.QtWebKitWidgets', []),
- ])
+ ]) # type: typing.Mapping[str, typing.Sequence[str]]
for modname, attributes in modules.items():
try:
module = importlib.import_module(modname)
@@ -239,7 +246,7 @@ def _module_versions():
return lines
-def _path_info():
+def _path_info() -> typing.Mapping[str, str]:
"""Get info about important path names.
Return:
@@ -258,7 +265,7 @@ def _path_info():
return info
-def _os_info():
+def _os_info() -> typing.Sequence[str]:
"""Get operating system info.
Return:
@@ -272,11 +279,11 @@ def _os_info():
elif utils.is_windows:
osver = ', '.join(platform.win32_ver())
elif utils.is_mac:
- release, versioninfo, machine = platform.mac_ver()
- if all(not e for e in versioninfo):
+ release, info_tpl, machine = platform.mac_ver()
+ if all(not e for e in info_tpl):
versioninfo = ''
else:
- versioninfo = '.'.join(versioninfo)
+ versioninfo = '.'.join(info_tpl)
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
elif utils.is_posix:
osver = ' '.join(platform.uname())
@@ -289,7 +296,7 @@ def _os_info():
return lines
-def _pdfjs_version():
+def _pdfjs_version() -> str:
"""Get the pdf.js version.
Return:
@@ -315,7 +322,7 @@ def _pdfjs_version():
return '{} ({})'.format(pdfjs_version, file_path)
-def _chromium_version():
+def _chromium_version() -> str:
"""Get the Chromium version for QtWebEngine.
This can also be checked by looking at this file with the right Qt tag:
@@ -368,7 +375,7 @@ def _chromium_version():
return match.group(1)
-def _backend():
+def _backend() -> str:
"""Get the backend line with relevant information."""
if objects.backend == usertypes.Backend.QtWebKit:
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
@@ -397,7 +404,7 @@ def _config_py_loaded() -> str:
return "no config.py was loaded"
-def version():
+def version() -> str:
"""Return a string with various version information."""
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str()
@@ -471,7 +478,7 @@ def version():
return '\n'.join(lines)
-def opengl_vendor(): # pragma: no cover
+def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
"""Get the OpenGL vendor used.
This returns a string such as 'nouveau' or
@@ -485,7 +492,8 @@ def opengl_vendor(): # pragma: no cover
log.init.debug("Using override {}".format(override))
return override
- old_context = QOpenGLContext.currentContext()
+ old_context = typing.cast(typing.Optional[QOpenGLContext],
+ QOpenGLContext.currentContext())
old_surface = None if old_context is None else old_context.surface()
surface = QOffscreenSurface()
@@ -527,20 +535,22 @@ def opengl_vendor(): # pragma: no cover
old_context.makeCurrent(old_surface)
-def pastebin_version(pbclient=None):
+def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
"""Pastebin the version and log the url to messages."""
- def _yank_url(url):
+ def _yank_url(url: str) -> None:
utils.set_clipboard(url)
message.info("Version url {} yanked to clipboard.".format(url))
- def _on_paste_version_success(url):
+ def _on_paste_version_success(url: str) -> None:
+ assert pbclient is not None
global pastebin_url
url = url.strip()
_yank_url(url)
pbclient.deleteLater()
pastebin_url = url
- def _on_paste_version_err(text):
+ def _on_paste_version_err(text: str) -> None:
+ assert pbclient is not None
message.error("Failed to pastebin version"
" info: {}".format(text))
pbclient.deleteLater()
diff --git a/requirements.txt b/requirements.txt
index 4d1279e87..5c8f4f385 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==19.1.0
+attrs==19.2.0
colorama==0.4.1
cssutils==1.0.2
-Jinja2==2.10.1
+Jinja2==2.10.3
MarkupSafe==1.1.1
Pygments==2.4.2
pyPEG2==2.15.2
diff --git a/scripts/dev/run_profile.py b/scripts/dev/run_profile.py
index e7ff564e2..e71c6aff0 100755
--- a/scripts/dev/run_profile.py
+++ b/scripts/dev/run_profile.py
@@ -49,6 +49,7 @@ def parse_args():
default='snakeviz',
help="The tool to use to view the profiling data")
parser.add_argument('--profile-file', metavar='FILE', action='store',
+ default="profile_data",
help="The filename to use with --profile-tool=none")
parser.add_argument('--profile-test', action='store_true',
help="Run pytest instead of qutebrowser")
@@ -80,7 +81,7 @@ def main():
profiler.dump_stats(profilefile)
if args.profile_tool == 'none':
- pass
+ print("Profile data written to {}".format(profilefile))
elif args.profile_tool == 'gprof2dot':
# yep, shell=True. I know what I'm doing.
subprocess.run(
diff --git a/scripts/utils.py b/scripts/utils.py
index d4e606ebc..6467c915c 100644
--- a/scripts/utils.py
+++ b/scripts/utils.py
@@ -52,14 +52,6 @@ fg_colors = {
bg_colors = {name: col + 10 for name, col in fg_colors.items()}
-term_attributes = {
- 'bright': 1,
- 'dim': 2,
- 'normal': 22,
- 'reset': 0,
-}
-
-
def _esc(code):
"""Get an ANSI color code based on a color number."""
return '\033[{}m'.format(code)
diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py
index 12bb789bd..01129c8ee 100644
--- a/tests/end2end/fixtures/webserver.py
+++ b/tests/end2end/fixtures/webserver.py
@@ -100,7 +100,7 @@ class Request(testprocess.Line):
return NotImplemented
-@attr.s(frozen=True, cmp=False, hash=True)
+@attr.s(frozen=True, eq=False, hash=True)
class ExpectedRequest:
"""Class to compare expected requests easily."""
diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py
index 1d68e26dc..df7cf18c0 100644
--- a/tests/end2end/test_dirbrowser.py
+++ b/tests/end2end/test_dirbrowser.py
@@ -27,7 +27,9 @@ import pytest
import bs4
from PyQt5.QtCore import QUrl
+
from qutebrowser.utils import urlutils
+from helpers import utils as testutils
pytestmark = pytest.mark.qtwebengine_skip("Title is empty when parsing for "
@@ -127,7 +129,10 @@ def parse(quteproc):
"""
html = quteproc.get_content(plain=False)
soup = bs4.BeautifulSoup(html, 'html.parser')
- print(soup.prettify())
+
+ with testutils.ignore_bs4_warning():
+ print(soup.prettify())
+
title_prefix = 'Browse directory: '
# Strip off the title prefix to obtain the path of the folder that
# we're browsing
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index d752456e6..596b5241b 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -47,8 +47,8 @@ from qutebrowser.config import (config, configdata, configtypes, configexc,
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
-from qutebrowser.misc import savemanager, sql, objects
+from qutebrowser.browser.webkit import cookies, cache
+from qutebrowser.misc import savemanager, sql, objects, sessions
from qutebrowser.keyinput import modeman
@@ -350,12 +350,11 @@ def bookmark_manager_stub(stubs):
@pytest.fixture
-def session_manager_stub(stubs):
+def session_manager_stub(stubs, monkeypatch):
"""Fixture which provides a fake session-manager object."""
stub = stubs.SessionManagerStub()
- objreg.register('session-manager', stub)
- yield stub
- objreg.delete('session-manager')
+ monkeypatch.setattr(sessions, 'session_manager', stub)
+ return stub
@pytest.fixture
@@ -370,15 +369,6 @@ def tabbed_browser_stubs(qapp, stubs, win_registry):
objreg.delete('tabbed-browser', scope='window', window=1)
-@pytest.fixture
-def app_stub(stubs):
- """Fixture which provides a fake app object."""
- stub = stubs.ApplicationStub()
- objreg.register('app', stub)
- yield stub
- objreg.delete('app')
-
-
@pytest.fixture
def status_command_stub(stubs, qtbot, win_registry):
"""Fixture which provides a fake status-command object."""
@@ -465,18 +455,11 @@ def webframe(webpage):
@pytest.fixture
-def cookiejar_and_cache(stubs):
+def cookiejar_and_cache(stubs, monkeypatch):
"""Fixture providing a fake cookie jar and cache."""
- jar = QNetworkCookieJar()
- ram_jar = cookies.RAMCookieJar()
- cache = stubs.FakeNetworkCache()
- objreg.register('cookie-jar', jar)
- objreg.register('ram-cookie-jar', ram_jar)
- objreg.register('cache', cache)
- yield
- objreg.delete('cookie-jar')
- objreg.delete('ram-cookie-jar')
- objreg.delete('cache')
+ monkeypatch.setattr(cookies, 'cookie_jar', QNetworkCookieJar())
+ monkeypatch.setattr(cookies, 'ram_cookie_jar', cookies.RAMCookieJar())
+ monkeypatch.setattr(cache, 'diskcache', stubs.FakeNetworkCache())
@pytest.fixture
@@ -501,18 +484,18 @@ def fake_save_manager():
@pytest.fixture
-def fake_args(request):
+def fake_args(request, monkeypatch):
ns = types.SimpleNamespace()
ns.backend = 'webengine' if request.config.webengine else 'webkit'
ns.debug_flags = []
- objreg.register('args', ns)
- yield ns
- objreg.delete('args')
+
+ monkeypatch.setattr(objects, 'args', ns)
+ return ns
@pytest.fixture
def mode_manager(win_registry, config_stub, key_config_stub, qapp):
- mm = modeman.init(0, parent=qapp)
+ mm = modeman.init(win_id=0, parent=qapp)
yield mm
objreg.delete('mode-manager', scope='window', window=0)
diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py
index cabd606b6..bb72dbccb 100644
--- a/tests/helpers/stubs.py
+++ b/tests/helpers/stubs.py
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
from qutebrowser.browser import browsertab, downloads
from qutebrowser.utils import usertypes
-from qutebrowser.mainwindow import mainwindow
+from qutebrowser.commands import runners
class FakeNetworkCache(QAbstractNetworkCache):
@@ -547,13 +547,6 @@ class TabWidgetStub(QObject):
return self.tabs[idx - 1]
-class ApplicationStub(QObject):
-
- """Stub to insert as the app object in objreg."""
-
- new_window = pyqtSignal(mainwindow.MainWindow)
-
-
class HTTPPostStub(QObject):
"""A stub class for HTTPClient.
@@ -639,3 +632,22 @@ class FakeHistoryProgress:
def finish(self):
self._finished = True
+
+
+class FakeCommandRunner(runners.AbstractCommandRunner):
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.commands = []
+
+ def run(self, text, count=None, *, safely=False):
+ self.commands.append((text, count))
+
+
+class FakeHintManager:
+
+ def __init__(self):
+ self.keystr = None
+
+ def handle_partial_key(self, keystr):
+ self.keystr = keystr
diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py
index 2157eaeb1..5be464a2c 100644
--- a/tests/helpers/utils.py
+++ b/tests/helpers/utils.py
@@ -27,7 +27,7 @@ import contextlib
import pytest
-from qutebrowser.utils import qtutils
+from qutebrowser.utils import qtutils, log
qt58 = pytest.mark.skipif(
@@ -180,3 +180,13 @@ def abs_datapath():
@contextlib.contextmanager
def nop_contextmanager():
yield
+
+
+@contextlib.contextmanager
+def ignore_bs4_warning():
+ """WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592."""
+ with log.ignore_py_warnings(
+ category=DeprecationWarning,
+ message="Using or importing the ABCs from 'collections' instead "
+ "of from 'collections.abc' is deprecated", module='bs4.element'):
+ yield
diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py
index 8ce677a25..3398db76a 100644
--- a/tests/unit/browser/test_hints.py
+++ b/tests/unit/browser/test_hints.py
@@ -54,8 +54,7 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
with qtbot.wait_signal(tab.load_finished):
tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
- manager = qutebrowser.browser.hints.HintManager(
- win_id=0, tab_id=tab.tab_id)
+ manager = qutebrowser.browser.hints.HintManager(win_id=0)
def bench():
with qtbot.wait_signal(mode_manager.entered):
@@ -76,8 +75,7 @@ def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
config_stub.val.hints.scatter = False
- manager = qutebrowser.browser.hints.HintManager(
- win_id=0, tab_id=tab.tab_id)
+ manager = qutebrowser.browser.hints.HintManager(win_id=0)
with qtbot.wait_signal(mode_manager.entered):
manager.start()
@@ -106,7 +104,7 @@ def test_scattered_hints_count(min_len, num_chars, num_elements):
2. There can only be two hint lengths, only 1 apart
3. There are no unique prefixes for long hints, such as 'la' with no 'l'
"""
- manager = qutebrowser.browser.hints.HintManager(0, 0)
+ manager = qutebrowser.browser.hints.HintManager(win_id=0)
chars = string.ascii_lowercase[:num_chars]
hints = manager._hint_scattered(min_len, chars,
diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py
index c477ead23..1372aa06c 100644
--- a/tests/unit/browser/webkit/network/test_filescheme.py
+++ b/tests/unit/browser/webkit/network/test_filescheme.py
@@ -28,6 +28,7 @@ from PyQt5.QtNetwork import QNetworkRequest
from qutebrowser.browser.webkit.network import filescheme
from qutebrowser.utils import urlutils, utils
+from helpers import utils as testutils
@pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [
@@ -129,7 +130,10 @@ class TestDirbrowserHtml:
def parse(path):
html = filescheme.dirbrowser_html(path).decode('utf-8')
soup = bs4.BeautifulSoup(html, 'html.parser')
- print(soup.prettify())
+
+ with testutils.ignore_bs4_warning():
+ print(soup.prettify())
+
container = soup('div', id='dirbrowserContainer')[0]
parent_elem = container('ul', class_='parent')
@@ -156,7 +160,10 @@ class TestDirbrowserHtml:
def test_basic(self):
html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
soup = bs4.BeautifulSoup(html, 'html.parser')
- print(soup.prettify())
+
+ with testutils.ignore_bs4_warning():
+ print(soup.prettify())
+
container = soup.div
assert container['id'] == 'dirbrowserContainer'
title_elem = container('div', id='dirbrowserTitle')[0]
@@ -170,7 +177,9 @@ class TestDirbrowserHtml:
html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
soup = bs4.BeautifulSoup(html, 'html.parser')
- print(soup.prettify())
+
+ with testutils.ignore_bs4_warning():
+ print(soup.prettify())
css = soup.html.head.style.string
assert "background-image: url('file:///test%20path/foo.svg');" in css
@@ -239,7 +248,10 @@ class TestDirbrowserHtml:
m.side_effect = OSError('Error message')
html = filescheme.dirbrowser_html('').decode('utf-8')
soup = bs4.BeautifulSoup(html, 'html.parser')
- print(soup.prettify())
+
+ with testutils.ignore_bs4_warning():
+ print(soup.prettify())
+
error_msg = soup('p', id='error-message-text')[0].string
assert error_msg == 'Error message'
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index 0ac91b8a8..1153b6976 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -37,7 +37,7 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
mocker.patch(
'qutebrowser.completion.completiondelegate.CompletionItemDelegate',
new=lambda *_: None)
- view = completionwidget.CompletionView(win_id=0)
+ view = completionwidget.CompletionView(cmd=status_command_stub, win_id=0)
qtbot.addWidget(view)
return view
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 3d3ca0bf3..cdba937b2 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -677,7 +677,7 @@ def test_session_completion(qtmodeltester, session_manager_stub):
})
-def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
+def test_tab_completion(qtmodeltester, fake_web_tab, win_registry,
tabbed_browser_stubs):
tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
@@ -703,8 +703,8 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
})
-def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
- win_registry, tabbed_browser_stubs):
+def test_tab_completion_delete(qtmodeltester, fake_web_tab, win_registry,
+ tabbed_browser_stubs):
"""Verify closing a tab by deleting it from the completion widget."""
tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
@@ -731,8 +731,8 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
QUrl('https://duckduckgo.com')]
-def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, app_stub,
- win_registry, tabbed_browser_stubs):
+def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, win_registry,
+ tabbed_browser_stubs):
"""Ensure that the completion row order is the same as tab index order.
Would be violated for more than 9 tabs if the completion was being
@@ -758,8 +758,8 @@ def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, app_stub,
})
-def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
- win_registry, tabbed_browser_stubs, info):
+def test_other_buffer_completion(qtmodeltester, fake_web_tab, win_registry,
+ tabbed_browser_stubs, info):
tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
@@ -782,7 +782,7 @@ def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
})
-def test_other_buffer_completion_id0(qtmodeltester, fake_web_tab, app_stub,
+def test_other_buffer_completion_id0(qtmodeltester, fake_web_tab,
win_registry, tabbed_browser_stubs, info):
tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
diff --git a/tests/unit/config/test_configexc.py b/tests/unit/config/test_configexc.py
index f3f86a6dd..0335d0cee 100644
--- a/tests/unit/config/test_configexc.py
+++ b/tests/unit/config/test_configexc.py
@@ -126,3 +126,9 @@ Fake traceback
""")
# Make sure the traceback is not indented
assert '\nFake traceback\n' in html
+
+
+def test_config_file_errors_fatal():
+ err = configexc.ConfigErrorDesc("Text", Exception("Text"))
+ errors = configexc.ConfigFileErrors("state", [err], fatal=True)
+ assert errors.fatal
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
index a21d6787a..833e1e4fd 100644
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -737,13 +737,27 @@ class TestConfigPy:
expected = {'normal': {'H': 'message-info back'}}
assert config.instance.get_obj('bindings.commands') == expected
- def test_bind_none(self, confpy):
+ def test_bind_nop(self, confpy):
confpy.write("c.bindings.commands = None",
"config.bind(',x', 'nop')")
confpy.read()
expected = {'normal': {',x': 'nop'}}
assert config.instance.get_obj('bindings.commands') == expected
+ def test_bind_none(self, confpy):
+ confpy.write("config.bind('', None)")
+ with pytest.raises(configexc.ConfigFileErrors) as excinfo:
+ confpy.read()
+
+ expected = {'normal': {'': None}}
+ assert config.instance.get_obj('bindings.commands') == expected
+
+ msg = ("While unbinding '': Unbinding commands with "
+ "config.bind('', None) is deprecated. Use "
+ "config.unbind('') instead.")
+ assert len(excinfo.value.errors) == 1
+ assert str(excinfo.value.errors[0]) == msg
+
@pytest.mark.parametrize('line, key, mode', [
('config.unbind("o")', 'o', 'normal'),
('config.unbind("y", mode="yesno")', 'y', 'yesno'),
diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py
index 93ca334ef..91264b758 100644
--- a/tests/unit/config/test_configinit.py
+++ b/tests/unit/config/test_configinit.py
@@ -101,7 +101,6 @@ class TestEarlyInit:
assert actual_errors == expected_errors
# Make sure things have been init'ed
- objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
assert isinstance(config.key_instance, config.KeyConfig)
@@ -205,6 +204,12 @@ class TestEarlyInit:
assert dump == '\n'.join(expected)
+ def test_state_init_errors(self, init_patch, args, data_tmpdir):
+ state_file = data_tmpdir / 'state'
+ state_file.write_binary(b'\x00')
+ configinit.early_init(args)
+ assert configinit._init_errors.errors
+
def test_invalid_change_filter(self, init_patch, args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
@@ -325,16 +330,23 @@ class TestEarlyInit:
configinit._init_envvars()
-@pytest.mark.parametrize('errors', [True, False])
+@pytest.mark.parametrize('errors', [True, 'fatal', False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
mocker, errors):
configinit.early_init(args)
+
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
+ if errors == 'fatal':
+ errs.fatal = True
+
monkeypatch.setattr(configinit, '_init_errors', errs)
+
msgbox_mock = mocker.patch('qutebrowser.config.configinit.msgbox.msgbox',
autospec=True)
+ exit_mock = mocker.patch('qutebrowser.config.configinit.sys.exit',
+ autospec=True)
configinit.late_init(fake_save_manager)
@@ -342,12 +354,15 @@ def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
'state-config', unittest.mock.ANY)
fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
+
if errors:
assert len(msgbox_mock.call_args_list) == 1
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
text = call_kwargs['text'].strip()
assert text.startswith('Errors occurred while reading config.py:')
assert 'Error text: Exception' in text
+
+ assert exit_mock.called == (errors == 'fatal')
else:
assert not msgbox_mock.called
diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py
index 133da3090..23e730349 100644
--- a/tests/unit/keyinput/test_basekeyparser.py
+++ b/tests/unit/keyinput/test_basekeyparser.py
@@ -36,7 +36,7 @@ def keyseq(s):
@pytest.fixture
def keyparser(key_config_stub):
"""Fixture providing a BaseKeyParser supporting count/chains."""
- kp = basekeyparser.BaseKeyParser(0, supports_count=True)
+ kp = basekeyparser.BaseKeyParser(win_id=0)
kp.execute = mock.Mock()
yield kp
@@ -79,7 +79,8 @@ class TestDebugLog:
])
def test_split_count(config_stub, key_config_stub,
input_key, supports_count, count, command):
- kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
+ kp = basekeyparser.BaseKeyParser(win_id=0)
+ kp.supports_count = supports_count
kp._read_config('normal')
for info in keyseq(input_key):
diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py
index 436843960..1b5533822 100644
--- a/tests/unit/keyinput/test_modeparsers.py
+++ b/tests/unit/keyinput/test_modeparsers.py
@@ -19,8 +19,6 @@
"""Tests for mode parsers."""
-from unittest import mock
-
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence
@@ -29,6 +27,11 @@ import pytest
from qutebrowser.keyinput import modeparsers, keyutils
+@pytest.fixture
+def commandrunner(stubs):
+ return stubs.FakeCommandRunner()
+
+
class TestsNormalKeyParser:
@pytest.fixture(autouse=True)
@@ -39,27 +42,27 @@ class TestsNormalKeyParser:
stubs.FakeTimer)
@pytest.fixture
- def keyparser(self):
- kp = modeparsers.NormalKeyParser(0)
- kp.execute = mock.Mock()
+ def keyparser(self, commandrunner):
+ kp = modeparsers.NormalKeyParser(win_id=0, commandrunner=commandrunner)
return kp
- def test_keychain(self, keyparser, fake_keyevent):
+ def test_keychain(self, keyparser, fake_keyevent, commandrunner):
"""Test valid keychain."""
- # Press 'x' which is ignored because of no match
- keyparser.handle(fake_keyevent(Qt.Key_X))
+ # Press 'z' which is ignored because of no match
+ keyparser.handle(fake_keyevent(Qt.Key_Z))
# Then start the real chain
keyparser.handle(fake_keyevent(Qt.Key_B))
keyparser.handle(fake_keyevent(Qt.Key_A))
- keyparser.execute.assert_called_with('message-info ba', None)
+ assert commandrunner.commands == [('message-info ba', None)]
assert not keyparser._sequence
def test_partial_keychain_timeout(self, keyparser, config_stub,
- fake_keyevent):
+ fake_keyevent, qtbot, commandrunner):
"""Test partial keychain timeout."""
config_stub.val.input.partial_timeout = 100
timer = keyparser._partial_timer
assert not timer.isActive()
+
# Press 'b' for a partial match.
# Then we check if the timer has been set up correctly
keyparser.handle(fake_keyevent(Qt.Key_B))
@@ -67,40 +70,57 @@ class TestsNormalKeyParser:
assert timer.interval() == 100
assert timer.isActive()
- assert not keyparser.execute.called
+ assert not commandrunner.commands
assert keyparser._sequence == keyutils.KeySequence.parse('b')
+
# Now simulate a timeout and check the keystring has been cleared.
- keystring_updated_mock = mock.Mock()
- keyparser.keystring_updated.connect(keystring_updated_mock)
- timer.timeout.emit()
- assert not keyparser.execute.called
+ with qtbot.wait_signal(keyparser.keystring_updated) as blocker:
+ timer.timeout.emit()
+
+ assert not commandrunner.commands
assert not keyparser._sequence
- keystring_updated_mock.assert_called_once_with('')
+ assert blocker.args == ['']
class TestHintKeyParser:
@pytest.fixture
- def keyparser(self, config_stub, key_config_stub):
- kp = modeparsers.HintKeyParser(0)
- kp.execute = mock.Mock()
- kp.keystring_updated.disconnect() # Don't try to update HintManager
- return kp
+ def hintmanager(self, stubs):
+ return stubs.FakeHintManager()
- def test_simple_hint_match(self, keyparser, fake_keyevent):
- keyparser.update_bindings(['aa', 'as'])
+ @pytest.fixture
+ def keyparser(self, config_stub, key_config_stub, commandrunner,
+ hintmanager):
+ return modeparsers.HintKeyParser(win_id=0,
+ hintmanager=hintmanager,
+ commandrunner=commandrunner)
- match = keyparser.handle(fake_keyevent(Qt.Key_A))
+ @pytest.mark.parametrize('bindings, event1, event2, prefix, command', [
+ (
+ ['aa', 'as'],
+ [Qt.Key_A],
+ [Qt.Key_S],
+ 'a',
+ 'follow-hint -s as'
+ ),
+ (
+ ['21', '22'],
+ [Qt.Key_2, Qt.KeypadModifier],
+ [Qt.Key_2, Qt.KeypadModifier],
+ '2',
+ 'follow-hint -s 22'
+ ),
+ ])
+ def test_match(self, keyparser, fake_keyevent, commandrunner, hintmanager,
+ bindings, event1, event2, prefix, command):
+ keyparser.update_bindings(bindings)
+
+ match = keyparser.handle(fake_keyevent(*event1))
assert match == QKeySequence.PartialMatch
- match = keyparser.handle(fake_keyevent(Qt.Key_S))
+ assert hintmanager.keystr == prefix
+
+ match = keyparser.handle(fake_keyevent(*event2))
assert match == QKeySequence.ExactMatch
+ assert not hintmanager.keystr
- keyparser.execute.assert_called_with('follow-hint -s as', None)
-
- def test_numberkey_hint_match(self, keyparser, fake_keyevent):
- keyparser.update_bindings(['21', '22'])
-
- match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
- assert match == QKeySequence.PartialMatch
- match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
- assert match == QKeySequence.ExactMatch
+ assert commandrunner.commands == [(command, None)]
diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py
index 8bb8155c6..611cfecbf 100644
--- a/tests/unit/misc/test_sessions.py
+++ b/tests/unit/misc/test_sessions.py
@@ -48,9 +48,13 @@ def sess_man(tmpdir):
class TestInit:
@pytest.fixture(autouse=True)
- def cleanup(self):
+ def cleanup(self, monkeypatch):
+ monkeypatch.setattr(sessions, 'session_manager', None)
yield
- objreg.delete('session-manager')
+ try:
+ objreg.delete('session-manager')
+ except KeyError:
+ pass
@pytest.mark.parametrize('create_dir', [True, False])
def test_with_standarddir(self, tmpdir, monkeypatch, create_dir):
@@ -60,10 +64,9 @@ class TestInit:
session_dir.ensure(dir=True)
sessions.init()
- manager = objreg.get('session-manager')
assert session_dir.exists()
- assert manager._base_path == str(session_dir)
+ assert sessions.session_manager._base_path == str(session_dir)
def test_did_not_load(sess_man):
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 89dcc4bf8..ad5a8f9ad 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -25,7 +25,7 @@ import collections
import os.path
import subprocess
import contextlib
-import builtins
+import builtins # noqa https://github.com/JBKahn/flake8-debugger/issues/20
import types
import importlib
import logging
diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py
index 13eb13e60..3a2000cba 100644
--- a/tests/unit/utils/usertypes/test_question.py
+++ b/tests/unit/utils/usertypes/test_question.py
@@ -41,12 +41,6 @@ def test_mode(question):
assert question.mode == usertypes.PromptMode.yesno
-def test_mode_invalid(question):
- """Test setting mode to something which is not a PromptMode member."""
- with pytest.raises(TypeError):
- question.mode = 42
-
-
@pytest.mark.parametrize('mode, answer, signal_names', [
(usertypes.PromptMode.text, 'foo', ['answered', 'completed']),
(usertypes.PromptMode.yesno, True, ['answered', 'answered_yes',